mirror of
https://gitee.com/ssssssss-team/magic-api.git
synced 2026-06-02 11:59:41 +08:00
docs(ai): 添加AI集成相关文档和代码
- 添加GitNexus代码智能文档AGENTS.md和CLAUDE.md - 新增AI模型选择类AiModelSelection用于运行时切换提供商和模型 - 实现AI服务管理器AiServiceManager支持动态选择AI提供商和模型 - 添加ClaudeService实现Anthropic API调用功能 - 创建SkillPromptLoader根据关键词动态加载技能提示词 - 添加基础提示词模板base-prompt.txt指导magic-script编写 - 实现进阶特性提示词skill-advanced.txt包含异步、Lambda等功能
This commit is contained in:
101
AGENTS.md
Normal file
101
AGENTS.md
Normal file
@@ -0,0 +1,101 @@
|
||||
<!-- gitnexus:start -->
|
||||
# GitNexus — Code Intelligence
|
||||
|
||||
This project is indexed by GitNexus as **magic-api** (6013 symbols, 20566 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
||||
|
||||
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
|
||||
|
||||
## Always Do
|
||||
|
||||
- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `gitnexus_impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user.
|
||||
- **MUST run `gitnexus_detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows.
|
||||
- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
|
||||
- When exploring unfamiliar code, use `gitnexus_query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
|
||||
- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `gitnexus_context({name: "symbolName"})`.
|
||||
|
||||
## When Debugging
|
||||
|
||||
1. `gitnexus_query({query: "<error or symptom>"})` — find execution flows related to the issue
|
||||
2. `gitnexus_context({name: "<suspect function>"})` — see all callers, callees, and process participation
|
||||
3. `READ gitnexus://repo/magic-api/process/{processName}` — trace the full execution flow step by step
|
||||
4. For regressions: `gitnexus_detect_changes({scope: "compare", base_ref: "main"})` — see what your branch changed
|
||||
|
||||
## When Refactoring
|
||||
|
||||
- **Renaming**: MUST use `gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})` first. Review the preview — graph edits are safe, text_search edits need manual review. Then run with `dry_run: false`.
|
||||
- **Extracting/Splitting**: MUST run `gitnexus_context({name: "target"})` to see all incoming/outgoing refs, then `gitnexus_impact({target: "target", direction: "upstream"})` to find all external callers before moving code.
|
||||
- After any refactor: run `gitnexus_detect_changes({scope: "all"})` to verify only expected files changed.
|
||||
|
||||
## Never Do
|
||||
|
||||
- NEVER edit a function, class, or method without first running `gitnexus_impact` on it.
|
||||
- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis.
|
||||
- NEVER rename symbols with find-and-replace — use `gitnexus_rename` which understands the call graph.
|
||||
- NEVER commit changes without running `gitnexus_detect_changes()` to check affected scope.
|
||||
|
||||
## Tools Quick Reference
|
||||
|
||||
| Tool | When to use | Command |
|
||||
|------|-------------|---------|
|
||||
| `query` | Find code by concept | `gitnexus_query({query: "auth validation"})` |
|
||||
| `context` | 360-degree view of one symbol | `gitnexus_context({name: "validateUser"})` |
|
||||
| `impact` | Blast radius before editing | `gitnexus_impact({target: "X", direction: "upstream"})` |
|
||||
| `detect_changes` | Pre-commit scope check | `gitnexus_detect_changes({scope: "staged"})` |
|
||||
| `rename` | Safe multi-file rename | `gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})` |
|
||||
| `cypher` | Custom graph queries | `gitnexus_cypher({query: "MATCH ..."})` |
|
||||
|
||||
## Impact Risk Levels
|
||||
|
||||
| Depth | Meaning | Action |
|
||||
|-------|---------|--------|
|
||||
| d=1 | WILL BREAK — direct callers/importers | MUST update these |
|
||||
| d=2 | LIKELY AFFECTED — indirect deps | Should test |
|
||||
| d=3 | MAY NEED TESTING — transitive | Test if critical path |
|
||||
|
||||
## Resources
|
||||
|
||||
| Resource | Use for |
|
||||
|----------|---------|
|
||||
| `gitnexus://repo/magic-api/context` | Codebase overview, check index freshness |
|
||||
| `gitnexus://repo/magic-api/clusters` | All functional areas |
|
||||
| `gitnexus://repo/magic-api/processes` | All execution flows |
|
||||
| `gitnexus://repo/magic-api/process/{name}` | Step-by-step execution trace |
|
||||
|
||||
## Self-Check Before Finishing
|
||||
|
||||
Before completing any code modification task, verify:
|
||||
1. `gitnexus_impact` was run for all modified symbols
|
||||
2. No HIGH/CRITICAL risk warnings were ignored
|
||||
3. `gitnexus_detect_changes()` confirms changes match expected scope
|
||||
4. All d=1 (WILL BREAK) dependents were updated
|
||||
|
||||
## Keeping the Index Fresh
|
||||
|
||||
After committing code changes, the GitNexus index becomes stale. Re-run analyze to update it:
|
||||
|
||||
```bash
|
||||
npx gitnexus analyze
|
||||
```
|
||||
|
||||
If the index previously included embeddings, preserve them by adding `--embeddings`:
|
||||
|
||||
```bash
|
||||
npx gitnexus analyze --embeddings
|
||||
```
|
||||
|
||||
To check whether embeddings exist, inspect `.gitnexus/meta.json` — the `stats.embeddings` field shows the count (0 means no embeddings). **Running analyze without `--embeddings` will delete any previously generated embeddings.**
|
||||
|
||||
> Claude Code users: A PostToolUse hook handles this automatically after `git commit` and `git merge`.
|
||||
|
||||
## CLI
|
||||
|
||||
| Task | Read this skill file |
|
||||
|------|---------------------|
|
||||
| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` |
|
||||
| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` |
|
||||
| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` |
|
||||
| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` |
|
||||
| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` |
|
||||
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` |
|
||||
|
||||
<!-- gitnexus:end -->
|
||||
101
CLAUDE.md
Normal file
101
CLAUDE.md
Normal file
@@ -0,0 +1,101 @@
|
||||
<!-- gitnexus:start -->
|
||||
# GitNexus — Code Intelligence
|
||||
|
||||
This project is indexed by GitNexus as **magic-api** (6013 symbols, 20566 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
||||
|
||||
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
|
||||
|
||||
## Always Do
|
||||
|
||||
- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `gitnexus_impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user.
|
||||
- **MUST run `gitnexus_detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows.
|
||||
- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
|
||||
- When exploring unfamiliar code, use `gitnexus_query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
|
||||
- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `gitnexus_context({name: "symbolName"})`.
|
||||
|
||||
## When Debugging
|
||||
|
||||
1. `gitnexus_query({query: "<error or symptom>"})` — find execution flows related to the issue
|
||||
2. `gitnexus_context({name: "<suspect function>"})` — see all callers, callees, and process participation
|
||||
3. `READ gitnexus://repo/magic-api/process/{processName}` — trace the full execution flow step by step
|
||||
4. For regressions: `gitnexus_detect_changes({scope: "compare", base_ref: "main"})` — see what your branch changed
|
||||
|
||||
## When Refactoring
|
||||
|
||||
- **Renaming**: MUST use `gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})` first. Review the preview — graph edits are safe, text_search edits need manual review. Then run with `dry_run: false`.
|
||||
- **Extracting/Splitting**: MUST run `gitnexus_context({name: "target"})` to see all incoming/outgoing refs, then `gitnexus_impact({target: "target", direction: "upstream"})` to find all external callers before moving code.
|
||||
- After any refactor: run `gitnexus_detect_changes({scope: "all"})` to verify only expected files changed.
|
||||
|
||||
## Never Do
|
||||
|
||||
- NEVER edit a function, class, or method without first running `gitnexus_impact` on it.
|
||||
- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis.
|
||||
- NEVER rename symbols with find-and-replace — use `gitnexus_rename` which understands the call graph.
|
||||
- NEVER commit changes without running `gitnexus_detect_changes()` to check affected scope.
|
||||
|
||||
## Tools Quick Reference
|
||||
|
||||
| Tool | When to use | Command |
|
||||
|------|-------------|---------|
|
||||
| `query` | Find code by concept | `gitnexus_query({query: "auth validation"})` |
|
||||
| `context` | 360-degree view of one symbol | `gitnexus_context({name: "validateUser"})` |
|
||||
| `impact` | Blast radius before editing | `gitnexus_impact({target: "X", direction: "upstream"})` |
|
||||
| `detect_changes` | Pre-commit scope check | `gitnexus_detect_changes({scope: "staged"})` |
|
||||
| `rename` | Safe multi-file rename | `gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})` |
|
||||
| `cypher` | Custom graph queries | `gitnexus_cypher({query: "MATCH ..."})` |
|
||||
|
||||
## Impact Risk Levels
|
||||
|
||||
| Depth | Meaning | Action |
|
||||
|-------|---------|--------|
|
||||
| d=1 | WILL BREAK — direct callers/importers | MUST update these |
|
||||
| d=2 | LIKELY AFFECTED — indirect deps | Should test |
|
||||
| d=3 | MAY NEED TESTING — transitive | Test if critical path |
|
||||
|
||||
## Resources
|
||||
|
||||
| Resource | Use for |
|
||||
|----------|---------|
|
||||
| `gitnexus://repo/magic-api/context` | Codebase overview, check index freshness |
|
||||
| `gitnexus://repo/magic-api/clusters` | All functional areas |
|
||||
| `gitnexus://repo/magic-api/processes` | All execution flows |
|
||||
| `gitnexus://repo/magic-api/process/{name}` | Step-by-step execution trace |
|
||||
|
||||
## Self-Check Before Finishing
|
||||
|
||||
Before completing any code modification task, verify:
|
||||
1. `gitnexus_impact` was run for all modified symbols
|
||||
2. No HIGH/CRITICAL risk warnings were ignored
|
||||
3. `gitnexus_detect_changes()` confirms changes match expected scope
|
||||
4. All d=1 (WILL BREAK) dependents were updated
|
||||
|
||||
## Keeping the Index Fresh
|
||||
|
||||
After committing code changes, the GitNexus index becomes stale. Re-run analyze to update it:
|
||||
|
||||
```bash
|
||||
npx gitnexus analyze
|
||||
```
|
||||
|
||||
If the index previously included embeddings, preserve them by adding `--embeddings`:
|
||||
|
||||
```bash
|
||||
npx gitnexus analyze --embeddings
|
||||
```
|
||||
|
||||
To check whether embeddings exist, inspect `.gitnexus/meta.json` — the `stats.embeddings` field shows the count (0 means no embeddings). **Running analyze without `--embeddings` will delete any previously generated embeddings.**
|
||||
|
||||
> Claude Code users: A PostToolUse hook handles this automatically after `git commit` and `git merge`.
|
||||
|
||||
## CLI
|
||||
|
||||
| Task | Read this skill file |
|
||||
|------|---------------------|
|
||||
| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` |
|
||||
| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` |
|
||||
| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` |
|
||||
| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` |
|
||||
| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` |
|
||||
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` |
|
||||
|
||||
<!-- gitnexus:end -->
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.ssssssss.magicapi.ai.model;
|
||||
|
||||
/**
|
||||
* AI模型选择,用于运行时切换提供商和模型
|
||||
*/
|
||||
public class AiModelSelection {
|
||||
|
||||
private String provider;
|
||||
|
||||
private String model;
|
||||
|
||||
public AiModelSelection() {
|
||||
}
|
||||
|
||||
public AiModelSelection(String provider, String model) {
|
||||
this.provider = provider;
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public String getProvider() {
|
||||
return provider;
|
||||
}
|
||||
|
||||
public void setProvider(String provider) {
|
||||
this.provider = provider;
|
||||
}
|
||||
|
||||
public String getModel() {
|
||||
return model;
|
||||
}
|
||||
|
||||
public void setModel(String model) {
|
||||
this.model = model;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package org.ssssssss.magicapi.ai.service;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.ssssssss.magicapi.ai.config.MagicAiProperties;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* AI服务管理器,支持按请求动态选择提供商和模型
|
||||
*/
|
||||
public class AiServiceManager {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AiServiceManager.class);
|
||||
|
||||
private final Map<String, MagicAiProperties.ProviderConfig> providerConfigs;
|
||||
|
||||
private final ConcurrentHashMap<String, AiService> serviceCache = new ConcurrentHashMap<>();
|
||||
|
||||
public AiServiceManager(MagicAiProperties properties) {
|
||||
this.providerConfigs = properties.getEffectiveProviders();
|
||||
logger.info("AI服务管理器初始化,已配置提供商: {}", providerConfigs.keySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据请求指定的提供商和模型获取AI服务实例
|
||||
*/
|
||||
public AiService getService(String provider, String model) {
|
||||
if (provider == null || provider.isEmpty()) {
|
||||
// 使用第一个配置的提供商作为默认
|
||||
provider = providerConfigs.keySet().iterator().next();
|
||||
}
|
||||
String cacheKey = provider + ":" + (model != null ? model : "");
|
||||
String finalProvider = provider;
|
||||
String finalModel = model;
|
||||
return serviceCache.computeIfAbsent(cacheKey, k -> createService(finalProvider, finalModel));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已配置的提供商信息(名称、标签、可选模型列表)
|
||||
*/
|
||||
public List<Map<String, Object>> getConfiguredProviders() {
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (Map.Entry<String, MagicAiProperties.ProviderConfig> entry : providerConfigs.entrySet()) {
|
||||
Map<String, Object> info = new LinkedHashMap<>();
|
||||
info.put("key", entry.getKey());
|
||||
MagicAiProperties.ProviderConfig config = entry.getValue();
|
||||
info.put("label", config.getLabel() != null ? config.getLabel() : entry.getKey());
|
||||
info.put("models", config.getModels() != null ? config.getModels() : Collections.emptyList());
|
||||
result.add(info);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private AiService createService(String provider, String model) {
|
||||
MagicAiProperties.ProviderConfig config = providerConfigs.get(provider);
|
||||
if (config == null) {
|
||||
throw new IllegalStateException("未找到提供商配置: " + provider);
|
||||
}
|
||||
String apiUrl = config.getApiUrl();
|
||||
String apiKey = config.getApiKey();
|
||||
// 优先使用请求指定的模型,否则使用提供商默认模型
|
||||
String effectiveModel = (model != null && !model.isEmpty()) ? model : config.getModel();
|
||||
|
||||
logger.info("创建AI服务实例: 提供商={}, 模型={}", provider, effectiveModel);
|
||||
|
||||
switch (provider.toLowerCase()) {
|
||||
case "dashscope":
|
||||
case "aliyun":
|
||||
return new DashScopeService(apiUrl, apiKey, effectiveModel);
|
||||
case "zhipu":
|
||||
case "glm":
|
||||
return new ZhipuAiService(apiUrl, apiKey, effectiveModel);
|
||||
case "minimax":
|
||||
return new MiniMaxService(apiUrl, apiKey, effectiveModel);
|
||||
case "deepseek":
|
||||
return new DeepSeekService(apiUrl, apiKey, effectiveModel);
|
||||
case "claude":
|
||||
case "anthropic":
|
||||
return new ClaudeService(apiUrl, apiKey, effectiveModel);
|
||||
case "openai":
|
||||
default:
|
||||
return new OpenAiService(apiUrl, apiKey, effectiveModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package org.ssssssss.magicapi.ai.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.ssssssss.magicapi.ai.model.AiChatMessage;
|
||||
import org.ssssssss.magicapi.ai.model.AiChatRequest;
|
||||
import org.ssssssss.magicapi.ai.model.AiChatResponse;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Claude (Anthropic) AI服务实现
|
||||
*/
|
||||
public class ClaudeService implements AiService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ClaudeService.class);
|
||||
|
||||
private final String apiUrl;
|
||||
private final String apiKey;
|
||||
private final String model;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
public ClaudeService(String apiUrl, String apiKey, String model) {
|
||||
this.apiUrl = apiUrl != null && !apiUrl.isEmpty() ? apiUrl : "https://api.anthropic.com/v1/messages";
|
||||
this.apiKey = apiKey;
|
||||
this.model = model != null && !model.isEmpty() ? model : "claude-sonnet-4-20250514";
|
||||
this.restTemplate = new RestTemplate();
|
||||
}
|
||||
|
||||
private String buildSystemPromptFull(AiChatRequest request) {
|
||||
StringBuilder systemPrompt = new StringBuilder(buildSystemPrompt(request.getMessage()));
|
||||
if (request.getCurrentCode() != null && !request.getCurrentCode().trim().isEmpty()) {
|
||||
systemPrompt.append("\n\n当前编辑器中的代码:\n```magicscript\n")
|
||||
.append(request.getCurrentCode())
|
||||
.append("\n```\n请在回答时结合以上代码上下文。");
|
||||
}
|
||||
return systemPrompt.toString();
|
||||
}
|
||||
|
||||
private ArrayNode buildMessages(AiChatRequest request) {
|
||||
ArrayNode messages = objectMapper.createArrayNode();
|
||||
// 历史消息(Claude messages 只支持 user 和 assistant)
|
||||
List<AiChatMessage> history = request.getHistory();
|
||||
if (history != null) {
|
||||
for (AiChatMessage msg : history) {
|
||||
String role = msg.getRole();
|
||||
if ("user".equals(role) || "assistant".equals(role)) {
|
||||
ObjectNode historyMsg = messages.addObject();
|
||||
historyMsg.put("role", role);
|
||||
historyMsg.put("content", msg.getContent());
|
||||
}
|
||||
}
|
||||
}
|
||||
// 当前用户消息
|
||||
ObjectNode userMsg = messages.addObject();
|
||||
userMsg.put("role", "user");
|
||||
userMsg.put("content", request.getMessage());
|
||||
return messages;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiChatResponse chat(AiChatRequest request) {
|
||||
try {
|
||||
ObjectNode requestBody = objectMapper.createObjectNode();
|
||||
requestBody.put("model", model);
|
||||
requestBody.put("max_tokens", 4096);
|
||||
requestBody.put("system", buildSystemPromptFull(request));
|
||||
requestBody.set("messages", buildMessages(request));
|
||||
|
||||
String jsonBody = objectMapper.writeValueAsString(requestBody);
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.set("x-api-key", apiKey);
|
||||
headers.set("anthropic-version", "2023-06-01");
|
||||
|
||||
HttpEntity<String> entity = new HttpEntity<>(jsonBody, headers);
|
||||
|
||||
ResponseEntity<String> response = restTemplate.postForEntity(apiUrl, entity, String.class);
|
||||
|
||||
if (response.getStatusCodeValue() != 200) {
|
||||
logger.error("Claude接口请求失败,状态码: {}", response.getStatusCodeValue());
|
||||
return new AiChatResponse("AI服务请求失败,状态码: " + response.getStatusCodeValue());
|
||||
}
|
||||
|
||||
JsonNode responseJson = objectMapper.readTree(response.getBody());
|
||||
// Claude 响应格式: { "content": [{"type": "text", "text": "..."}] }
|
||||
JsonNode contentArray = responseJson.path("content");
|
||||
StringBuilder contentBuilder = new StringBuilder();
|
||||
if (contentArray.isArray()) {
|
||||
for (JsonNode block : contentArray) {
|
||||
if ("text".equals(block.path("type").asText())) {
|
||||
contentBuilder.append(block.path("text").asText(""));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new AiChatResponse(contentBuilder.toString());
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Claude服务调用异常", e);
|
||||
return new AiChatResponse("AI服务调用异常: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void chatStream(AiChatRequest request, Consumer<String> onToken) {
|
||||
try {
|
||||
ObjectNode requestBody = objectMapper.createObjectNode();
|
||||
requestBody.put("model", model);
|
||||
requestBody.put("max_tokens", 4096);
|
||||
requestBody.put("stream", true);
|
||||
requestBody.put("system", buildSystemPromptFull(request));
|
||||
requestBody.set("messages", buildMessages(request));
|
||||
|
||||
String jsonBody = objectMapper.writeValueAsString(requestBody);
|
||||
|
||||
HttpURLConnection conn = (HttpURLConnection) new URL(apiUrl).openConnection();
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setDoOutput(true);
|
||||
conn.setRequestProperty("Content-Type", "application/json");
|
||||
conn.setRequestProperty("x-api-key", apiKey);
|
||||
conn.setRequestProperty("anthropic-version", "2023-06-01");
|
||||
conn.setRequestProperty("Accept", "text/event-stream");
|
||||
|
||||
try (OutputStream os = conn.getOutputStream()) {
|
||||
os.write(jsonBody.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.startsWith("data: ")) {
|
||||
String data = line.substring(6).trim();
|
||||
JsonNode chunk = objectMapper.readTree(data);
|
||||
String type = chunk.path("type").asText("");
|
||||
|
||||
if ("content_block_delta".equals(type)) {
|
||||
String text = chunk.path("delta").path("text").asText(null);
|
||||
if (text != null) {
|
||||
onToken.accept(text);
|
||||
}
|
||||
} else if ("message_stop".equals(type)) {
|
||||
break;
|
||||
} else if ("error".equals(type)) {
|
||||
String errMsg = chunk.path("error").path("message").asText("未知错误");
|
||||
logger.error("Claude流式错误: {}", errMsg);
|
||||
onToken.accept("\n\nClaude错误: " + errMsg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Claude流式调用异常", e);
|
||||
onToken.accept("\n\nAI服务流式调用异常: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package org.ssssssss.magicapi.ai.service;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Skill 提示词加载器:根据用户消息关键词匹配,动态加载对应的 Skill 提示词
|
||||
*/
|
||||
public class SkillPromptLoader {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SkillPromptLoader.class);
|
||||
|
||||
private static final String PROMPT_DIR = "prompt/";
|
||||
private static final int MAX_SKILLS = 3;
|
||||
|
||||
/** 缓存:文件名 → 文件内容 */
|
||||
private static final Map<String, String> PROMPT_CACHE = new LinkedHashMap<>();
|
||||
|
||||
/** Skill 关键词映射:文件名 → 关键词列表 */
|
||||
private static final Map<String, List<String>> SKILL_KEYWORDS = new LinkedHashMap<>();
|
||||
|
||||
static {
|
||||
// 初始化关键词映射
|
||||
SKILL_KEYWORDS.put("skill-db-sql", Arrays.asList(
|
||||
"查询", "select", "查找", "列表", "分页", "page", "新增", "insert", "添加",
|
||||
"修改", "update", "更新", "删除", "delete", "selectOne", "单条", "count",
|
||||
"selectInt", "selectValue", "数据库", "sql", "SQL"));
|
||||
|
||||
SKILL_KEYWORDS.put("skill-db-table", Arrays.asList(
|
||||
"table", "表", "eq", "ne", "gt", "lt", "like", "where", "orderBy",
|
||||
"groupBy", "primary", "save", "batchInsert", "批量插入", "exists",
|
||||
"db.table", "链式", "fluent"));
|
||||
|
||||
SKILL_KEYWORDS.put("skill-dynamic-sql", Arrays.asList(
|
||||
"动态", "dynamic", "mybatis", "<if", "<where", "<set", "<foreach",
|
||||
"条件查询", "模糊", "搜索", "条件", "动态SQL", "动态sql"));
|
||||
|
||||
SKILL_KEYWORDS.put("skill-transaction-cache", Arrays.asList(
|
||||
"事务", "transaction", "commit", "rollback", "缓存", "cache",
|
||||
"多数据源", "slave", "从库", "数据源"));
|
||||
|
||||
SKILL_KEYWORDS.put("skill-request-response", Arrays.asList(
|
||||
"请求", "参数", "header", "body", "path", "cookie", "getFile",
|
||||
"上传", "文件", "response", "download", "下载", "exit", "验证",
|
||||
"校验", "接口", "CRUD", "crud"));
|
||||
|
||||
SKILL_KEYWORDS.put("skill-http-client", Arrays.asList(
|
||||
"http", "调用", "请求外部", "connect", "第三方", "API", "api",
|
||||
"远程", "外部接口", "对接", "http请求", "HTTP请求"));
|
||||
|
||||
SKILL_KEYWORDS.put("skill-collection-ops", Arrays.asList(
|
||||
"集合", "数组", "map", "filter", "group", "join", "sort", "distinct",
|
||||
"reduce", "列表操作", "转换", "聚合", "sum", "avg", "tree", "树",
|
||||
"分组", "排序", "去重", "遍历", "toMap"));
|
||||
|
||||
SKILL_KEYWORDS.put("skill-linq", Arrays.asList(
|
||||
"linq", "LINQ", "select.*from", "group by", "order by", "left join",
|
||||
"关联查询", "内存查询", "集合查询"));
|
||||
|
||||
SKILL_KEYWORDS.put("skill-java-interop", Arrays.asList(
|
||||
"java", "Java", "import", "Date", "Spring", "Bean", "bean",
|
||||
"类型转换", "::int", "::date", "asString", "asInt", "new ",
|
||||
"调用Java", "调用java", "自定义函数"));
|
||||
|
||||
SKILL_KEYWORDS.put("skill-advanced", Arrays.asList(
|
||||
"异步", "async", "lambda", "箭头函数", "可选链", "?.",
|
||||
"展开", "...", "log", "日志", "env", "环境变量",
|
||||
"try", "catch", "redis", "Redis", "range"));
|
||||
|
||||
// 加载所有提示词文件
|
||||
loadAllPrompts();
|
||||
}
|
||||
|
||||
private static void loadAllPrompts() {
|
||||
// 加载 base prompt
|
||||
loadPromptFile("base-prompt");
|
||||
|
||||
// 加载所有 skill 文件
|
||||
for (String skillName : SKILL_KEYWORDS.keySet()) {
|
||||
loadPromptFile(skillName);
|
||||
}
|
||||
}
|
||||
|
||||
private static void loadPromptFile(String name) {
|
||||
String path = PROMPT_DIR + name + ".txt";
|
||||
try (InputStream is = SkillPromptLoader.class.getClassLoader().getResourceAsStream(path)) {
|
||||
if (is == null) {
|
||||
logger.warn("提示词文件未找到: {}", path);
|
||||
return;
|
||||
}
|
||||
String content = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))
|
||||
.lines()
|
||||
.collect(Collectors.joining("\n"));
|
||||
PROMPT_CACHE.put(name, content);
|
||||
logger.debug("已加载提示词文件: {}", name);
|
||||
} catch (Exception e) {
|
||||
logger.error("加载提示词文件失败: {}", path, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户消息构建完整的系统提示词
|
||||
*
|
||||
* @param userMessage 用户消息
|
||||
* @return 组装后的系统提示词(base + 匹配的 skills)
|
||||
*/
|
||||
public static String buildPrompt(String userMessage) {
|
||||
StringBuilder prompt = new StringBuilder();
|
||||
|
||||
// 始终加载 base prompt
|
||||
String basePrompt = PROMPT_CACHE.get("base-prompt");
|
||||
if (basePrompt != null) {
|
||||
prompt.append(basePrompt);
|
||||
}
|
||||
|
||||
// 匹配 skills
|
||||
List<String> matchedSkills = matchSkills(userMessage);
|
||||
|
||||
for (String skill : matchedSkills) {
|
||||
String skillContent = PROMPT_CACHE.get(skill);
|
||||
if (skillContent != null) {
|
||||
prompt.append("\n\n").append(skillContent);
|
||||
}
|
||||
}
|
||||
|
||||
return prompt.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户消息匹配最相关的 skill(最多 MAX_SKILLS 个)
|
||||
*/
|
||||
static List<String> matchSkills(String userMessage) {
|
||||
if (userMessage == null || userMessage.trim().isEmpty()) {
|
||||
// 默认加载最常用的 skill
|
||||
return Arrays.asList("skill-db-sql", "skill-request-response");
|
||||
}
|
||||
|
||||
String msgLower = userMessage.toLowerCase();
|
||||
|
||||
// 计算每个 skill 的关键词命中数
|
||||
Map<String, Integer> hitCounts = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, List<String>> entry : SKILL_KEYWORDS.entrySet()) {
|
||||
int count = 0;
|
||||
for (String keyword : entry.getValue()) {
|
||||
if (msgLower.contains(keyword.toLowerCase())) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
if (count > 0) {
|
||||
hitCounts.put(entry.getKey(), count);
|
||||
}
|
||||
}
|
||||
|
||||
if (hitCounts.isEmpty()) {
|
||||
// 无匹配时,加载默认 skill
|
||||
return Arrays.asList("skill-db-sql", "skill-request-response");
|
||||
}
|
||||
|
||||
// 按命中数降序排列,取 top N
|
||||
return hitCounts.entrySet().stream()
|
||||
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
|
||||
.limit(MAX_SKILLS)
|
||||
.map(Map.Entry::getKey)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
你是一个 magic-api 脚本专家,专门帮助用户编写 magic-script 脚本代码。
|
||||
|
||||
magic-script 是 magic-api 框架的脚本引擎,语法类似 JavaScript 但不是 JavaScript,它有自己独特的语法和 API,你必须严格遵守。
|
||||
|
||||
## 核心语法
|
||||
- 变量定义:var name = 'hello'(var 可省略);const val = 1(常量);语句末尾分号可选
|
||||
- 字符串插值:`Hello ${name}`
|
||||
- 多行字符串:三引号 """..."""(常用于 SQL)
|
||||
- 条件:if / else if / else
|
||||
- 循环:for(item in list)、for(key, value in map)、for(i in range(0, 10))、while
|
||||
- Lambda:e => e + 1、(a, b) => a + b、(a, b) => { return a + b }
|
||||
- 可选链:obj?.prop(不抛异常,返回 null)
|
||||
- 展开运算符:{...map, key: val}、[...list]
|
||||
- 类型转换:value::int、value::string、value::date('yyyy-MM-dd')、value::json、value::stringify
|
||||
- 扩展方法:value.asString()、value.asInt()、123.45.toFixed(2)、0.5.asPercent(2)
|
||||
- exit 语句:exit 400, '错误信息' 直接终止并返回指定状态码
|
||||
- try/catch/finally:catch(e) 中用 e.getMessage() 获取错误信息
|
||||
|
||||
## 请求参数获取(非常重要)
|
||||
magic-api 会自动将请求参数注入脚本作用域,直接用变量名访问:
|
||||
- GET 参数 /user?name=xx:直接写 name 即可拿到值
|
||||
- 路径变量 /user/{id}:直接写 id 即可拿到值
|
||||
- POST 请求体:body 变量是整个请求体对象,body.name 访问字段
|
||||
- 请求头:header.Authorization、header.token
|
||||
- 路径变量(显式):path.id
|
||||
|
||||
## 数据库参数绑定(非常重要)
|
||||
SQL 中用 #{varName} 引用当前作用域的变量,不需要传参数 map:
|
||||
```magicscript
|
||||
var name = '张三'
|
||||
var age = 25
|
||||
// #{name} 和 #{age} 自动从作用域取值,不需要额外传参
|
||||
db.insert('insert into user(name, age) values(#{name}, #{age})')
|
||||
```
|
||||
|
||||
## 内置模块(必须用 import 导入后才能使用)
|
||||
- import log → log.info('msg: {}', arg)
|
||||
- import env → env.get('server.port')
|
||||
- import http → HTTP 客户端
|
||||
- import request → request.getParameter('name')、request.getFile('file')
|
||||
- import response → response.json()、response.download()
|
||||
- import redis → Redis 操作(插件)
|
||||
|
||||
## Java 类导入
|
||||
import 'java.util.Date' as Date → var now = new Date()
|
||||
import 'java.text.SimpleDateFormat' as Fmt → new Fmt('yyyy-MM-dd').format(date)
|
||||
|
||||
## ⚠ 绝对禁止的错误写法(必须严格遵守)
|
||||
|
||||
以下写法在 magic-script 中不存在,使用会导致脚本报错:
|
||||
|
||||
1. db 方法不接受参数 map:
|
||||
❌ db.select(sql, {key: value})
|
||||
✅ var key = value; db.select('... #{key} ...')
|
||||
|
||||
2. 不存在 query 对象:
|
||||
❌ query.name、query.city
|
||||
✅ 直接用变量名 name(GET参数自动注入作用域)
|
||||
|
||||
3. 不存在 request.parameters:
|
||||
❌ request.parameters.name
|
||||
✅ 直接用变量名 name,或 import request 后 request.getParameter('name')
|
||||
|
||||
4. 使用 Java 类必须先 import:
|
||||
❌ new Date()、new SimpleDateFormat()(未导入直接用)
|
||||
✅ import 'java.util.Date' as Date; var now = new Date()
|
||||
|
||||
5. 模块必须先 import 才能使用:
|
||||
❌ env.get('key')(未导入直接用)
|
||||
✅ import env; env.get('key')
|
||||
|
||||
6. 不支持 JavaScript 原生方法和对象:
|
||||
❌ .toLocaleString()、JSON.stringify()、JSON.parse()、Math.ceil()、Object.keys()、Object.values()、Array.isArray()
|
||||
✅ 用 magic-script 扩展方法:obj::stringify、str::json、num.ceil()
|
||||
✅ 遍历 map 用 for(key, value in map),判断类型用 value::int 转换
|
||||
|
||||
7. catch 中用 Java 方法:
|
||||
❌ e.message
|
||||
✅ e.getMessage()
|
||||
|
||||
8. 不支持 Elvis 运算符:
|
||||
❌ a ?: b
|
||||
✅ a != null ? a : b
|
||||
|
||||
9. 分页用 db.page():
|
||||
❌ 自己写 LIMIT #{offset}, #{size}
|
||||
✅ db.page('select ... from ...')(自动处理分页)
|
||||
|
||||
10. 不要手动包装返回格式:
|
||||
❌ return {code: 200, message: 'success', data: result}
|
||||
✅ return result(框架自动包装为 {code:1, message:'success', data: result})
|
||||
|
||||
11. 对 body 字段做类型判断要小心:
|
||||
❌ body.id.trim()(如果 id 是数字会报错)
|
||||
✅ body.id == null 或 !body.id(判断是否为空)
|
||||
✅ 如果需要判断字符串为空:body.name == null || body.name == ''
|
||||
|
||||
12. 自增主键的表不要用 uuid():
|
||||
❌ db.table('xxx').primary('id', () => uuid()).insert(data)(主键是自增整数时)
|
||||
✅ db.table('xxx').primary('id').insert(data)(自增主键不需要指定生成策略)
|
||||
|
||||
13. 不要用 now() 函数(不存在):
|
||||
❌ update_time = now()(magic-script 没有 now()函数)
|
||||
✅ 用 SQL 的 now():update user set update_time = now() where id = #{id}
|
||||
✅ 用 Java:import 'java.util.Date' as Date; var now = new Date()
|
||||
|
||||
14. LINQ return 语句要连写:
|
||||
❌ return\nselect xxx from ...(return 和 select 分行)
|
||||
✅ return select xxx from ...(return 和 select 在同一行)
|
||||
|
||||
15. map对象不能用 delete 语句删除字段:
|
||||
❌ delete updateData.id(delete 关键字不能用于删除 map 字段)
|
||||
✅ 用筛选重建新map:var updateData = body.filter((k, v) => k != 'id')
|
||||
|
||||
## 输出规则
|
||||
- 代码用 ```magicscript 包裹
|
||||
- 脚本顶层直接执行,不需要 main 函数
|
||||
- 最终结果用 return 返回
|
||||
- 注释简洁,使用中文注释
|
||||
- 直接 return 数据,框架自动包装为 {code:1, message:'success', data: 你的返回值}
|
||||
|
||||
## 标准代码模式参考
|
||||
查询列表+分页+条件搜索:
|
||||
```magicscript
|
||||
return db.page("""
|
||||
select * from user
|
||||
<where>
|
||||
<if test="name != null and name != ''">and name like concat('%',#{name},'%')</if>
|
||||
<if test="status != null">and status = #{status}</if>
|
||||
</where>
|
||||
order by create_time desc
|
||||
""")
|
||||
```
|
||||
|
||||
新增接口(自增主键):
|
||||
```magicscript
|
||||
if (!body.name || body.name == '') {
|
||||
exit 400, '名称不能为空'
|
||||
}
|
||||
return db.table('user').primary('id').insert(body)
|
||||
```
|
||||
|
||||
根据ID查询:
|
||||
```magicscript
|
||||
var user = db.selectOne('select * from user where id = #{id}')
|
||||
if (!user) {
|
||||
exit 404, '用户不存在'
|
||||
}
|
||||
return user
|
||||
```
|
||||
|
||||
调用外部接口:
|
||||
```magicscript
|
||||
import http
|
||||
var result = http.connect('https://api.example.com/data')
|
||||
.header('Authorization', 'Bearer ' + token)
|
||||
.param('city', city)
|
||||
.get()
|
||||
.getBody()
|
||||
return result
|
||||
```
|
||||
@@ -0,0 +1,124 @@
|
||||
## 进阶特性
|
||||
|
||||
### 异步执行
|
||||
```magicscript
|
||||
// async 包裹的代码块在新线程执行,返回 Future
|
||||
var future = async () => {
|
||||
return db.select('select * from big_table')
|
||||
}
|
||||
// 其他操作...
|
||||
var result = future.get() // 阻塞获取结果
|
||||
|
||||
// 异步 + 循环(注意参数传递保证线程安全)
|
||||
var futures = []
|
||||
for(id in ids) {
|
||||
futures.add(async (id) => db.selectOne('select * from user where id = #{id}'))
|
||||
}
|
||||
return futures.map(f => f.get())
|
||||
```
|
||||
|
||||
### Lambda 函数
|
||||
```magicscript
|
||||
// 单参数单表达式
|
||||
var double = e => e * 2
|
||||
|
||||
// 多参数
|
||||
var add = (a, b) => a + b
|
||||
|
||||
// 带代码块
|
||||
var process = (item) => {
|
||||
var result = item.value * 2
|
||||
return {name: item.name, value: result}
|
||||
}
|
||||
|
||||
// 作为参数传递
|
||||
var list = [3, 1, 4, 1, 5]
|
||||
list.sort((a, b) => a - b)
|
||||
list.filter(it => it > 2)
|
||||
list.map(it => it * 10)
|
||||
```
|
||||
|
||||
### 可选链 ?.
|
||||
```magicscript
|
||||
// ?. 遇到 null 返回 null,不抛异常
|
||||
var city = user?.address?.city // 安全访问嵌套属性
|
||||
var name = list?.first()?.name
|
||||
|
||||
// 对比:. 遇到 null 抛出异常
|
||||
// var city = user.address.city // address 为 null 时报错
|
||||
```
|
||||
|
||||
### 展开运算符 ...
|
||||
```magicscript
|
||||
// 展开 Map
|
||||
var base = {name: '张三', age: 20}
|
||||
var extended = {...base, dept: 'IT', age: 25} // age 被覆盖为 25
|
||||
|
||||
// 展开 List
|
||||
var list1 = [1, 2, 3]
|
||||
var list2 = [0, ...list1, 4, 5] // [0, 1, 2, 3, 4, 5]
|
||||
```
|
||||
|
||||
### 日志
|
||||
```magicscript
|
||||
import log;
|
||||
|
||||
log.info('用户{}登录成功', username)
|
||||
log.warn('库存不足: 商品{}, 剩余{}', productId, stock)
|
||||
log.error('操作失败', exception)
|
||||
log.debug('调试信息: {}', data)
|
||||
```
|
||||
|
||||
### 环境变量
|
||||
```magicscript
|
||||
import env;
|
||||
|
||||
// 读取 Spring 配置
|
||||
var port = env.get('server.port')
|
||||
var dbUrl = env.get('spring.datasource.url')
|
||||
var customVal = env.get('app.custom.config')
|
||||
```
|
||||
|
||||
### try / catch / finally
|
||||
```magicscript
|
||||
try {
|
||||
var result = db.update('update account set balance = balance - 100 where id = 1')
|
||||
if (result == 0) {
|
||||
exit 400, '账户不存在'
|
||||
}
|
||||
return '成功'
|
||||
} catch(e) {
|
||||
log.error('操作异常', e)
|
||||
exit 500, '系统异常: ' + e.getMessage()
|
||||
} finally {
|
||||
log.info('操作完成')
|
||||
}
|
||||
```
|
||||
|
||||
### range 函数
|
||||
```magicscript
|
||||
// range(start, end) 生成序列(包含首尾)
|
||||
for(i in range(1, 10)) {
|
||||
log.info('第{}次', i)
|
||||
}
|
||||
|
||||
// 常用于批量生成测试数据
|
||||
var testData = range(1, 100).map(i => {
|
||||
name: '用户' + i,
|
||||
age: 20 + (i % 30)
|
||||
})
|
||||
```
|
||||
|
||||
### Redis 操作(插件)
|
||||
```magicscript
|
||||
import redis;
|
||||
|
||||
// 设置值(带过期时间,秒)
|
||||
redis.setex('user:token:' + userId, 3600, token)
|
||||
|
||||
// 获取值
|
||||
var token = redis.get('user:token:' + userId)
|
||||
|
||||
// 删除
|
||||
redis.del('user:token:' + userId)
|
||||
```
|
||||
@@ -0,0 +1,114 @@
|
||||
## 集合操作
|
||||
|
||||
magic-script 为 List 和 Map 提供了丰富的链式操作方法。
|
||||
|
||||
### List 遍历与转换
|
||||
```magicscript
|
||||
var list = [{name: '张三', age: 20, dept: 'IT'}, {name: '李四', age: 30, dept: 'HR'}]
|
||||
|
||||
// 遍历
|
||||
list.each(it => log.info('name: {}', it.name))
|
||||
|
||||
// 映射转换
|
||||
var names = list.map(it => it.name)
|
||||
// ['张三', '李四']
|
||||
|
||||
// 过滤
|
||||
var young = list.filter(it => it.age < 25)
|
||||
|
||||
// 查找
|
||||
var found = list.find(it => it.name == '张三')
|
||||
```
|
||||
|
||||
### 排序
|
||||
```magicscript
|
||||
// 自然排序
|
||||
var sorted = list.sort((a, b) => a.age - b.age)
|
||||
|
||||
// 反转
|
||||
var reversed = list.reverse()
|
||||
|
||||
// 去重
|
||||
var unique = list.distinct()
|
||||
```
|
||||
|
||||
### 聚合
|
||||
```magicscript
|
||||
var ages = [20, 25, 30, 35, 40]
|
||||
ages.sum() // 150
|
||||
ages.avg() // 30
|
||||
ages.max() // 40
|
||||
ages.min() // 20
|
||||
ages.first() // 20
|
||||
ages.last() // 40
|
||||
list.size() // 5
|
||||
```
|
||||
|
||||
### 截取
|
||||
```magicscript
|
||||
list.skip(2) // 跳过前2个
|
||||
list.limit(5) // 取前5个
|
||||
list.skip(2).limit(5) // 跳过2个取5个
|
||||
list.concat(list2) // 合并列表
|
||||
```
|
||||
|
||||
### 分组
|
||||
```magicscript
|
||||
// 按部门分组
|
||||
var grouped = list.group(it => it.dept)
|
||||
// {IT: [{...}], HR: [{...}]}
|
||||
|
||||
// 分组并聚合
|
||||
var deptCount = list.group(it => it.dept, items => items.size())
|
||||
// {IT: 3, HR: 2}
|
||||
```
|
||||
|
||||
### 提示:分组统计优先用 SQL
|
||||
如果只需要分组统计数量,优先使用 SQL 的 GROUP BY,比内存分组更高效:
|
||||
```magicscript
|
||||
// 推荐:直接用 SQL 分组统计
|
||||
return db.select('select dept_id, count(*) as count from user group by dept_id order by count desc')
|
||||
```
|
||||
|
||||
### 关联(Join)
|
||||
```magicscript
|
||||
var users = db.select('select * from user')
|
||||
var depts = db.select('select * from department')
|
||||
|
||||
// 关联两个列表
|
||||
var result = users.join(depts, (u, d) => u.dept_id == d.id)
|
||||
.map(it => {
|
||||
name: it.name,
|
||||
deptName: it.dept_name
|
||||
})
|
||||
```
|
||||
|
||||
### Map 操作
|
||||
```magicscript
|
||||
var map = {a: 1, b: 2, c: 3}
|
||||
map.each((key, value) => log.info('{}: {}', key, value))
|
||||
|
||||
// Map 转 List
|
||||
var list = map.asList((key, value) => {k: key, v: value})
|
||||
|
||||
// Map 排序
|
||||
map.sort((k1, k2, v1, v2) => v2 - v1)
|
||||
```
|
||||
|
||||
### List 转 Map
|
||||
```magicscript
|
||||
var userMap = list.toMap(it => it.id)
|
||||
// {1: {id:1, name:'张三'}, 2: {id:2, name:'李四'}}
|
||||
```
|
||||
|
||||
### 树结构转换
|
||||
```magicscript
|
||||
var toTree = (list, parentId) => {
|
||||
return list.filter(it => it.parent_id == parentId)
|
||||
.each(it => {
|
||||
it.children = toTree(list, it.id)
|
||||
})
|
||||
}
|
||||
var allDepts = db.select('select id, name, parent_id from department')
|
||||
return toTree(allDepts, '0')
|
||||
```
|
||||
@@ -0,0 +1,79 @@
|
||||
## 数据库 SQL 操作(db 模块)
|
||||
|
||||
db 模块无需导入,直接使用。
|
||||
|
||||
### 参数绑定规则(重要)
|
||||
- #{varName} 是预编译占位符,自动从当前作用域取变量值,防 SQL 注入
|
||||
- ${varName} 是字符串拼接,不防注入,慎用
|
||||
- db 的所有方法只接受 SQL 字符串,不接受参数 map!参数通过作用域变量传递
|
||||
|
||||
```magicscript
|
||||
// ✅ 正确写法:先定义变量,SQL 中用 #{} 引用
|
||||
var name = body.name
|
||||
var status = 1
|
||||
db.select('select * from user where name = #{name} and status = #{status}')
|
||||
|
||||
// ❌ 错误写法:传参数 map(不支持!)
|
||||
// db.select('select * from user where name = #{name}', {name: 'xx'})
|
||||
```
|
||||
|
||||
### 查询
|
||||
```magicscript
|
||||
// 查询列表
|
||||
var list = db.select('select * from user where status = #{status}')
|
||||
|
||||
// 分页查询(自动从请求参数获取 page/size)
|
||||
var page = db.page('select * from user order by create_time desc')
|
||||
|
||||
// 查询单条
|
||||
var user = db.selectOne('select * from user where id = #{id}')
|
||||
|
||||
// 查询单个值
|
||||
var count = db.selectInt('select count(*) from user')
|
||||
var name = db.selectValue('select name from user where id = #{id}')
|
||||
```
|
||||
|
||||
### 增删改
|
||||
```magicscript
|
||||
// 新增(返回影响行数)
|
||||
db.insert('insert into user(name, age) values(#{name}, #{age})')
|
||||
|
||||
// 新增并返回自增主键
|
||||
var newId = db.insert('insert into user(name) values(#{name})', 'id')
|
||||
|
||||
// 更新
|
||||
db.update('update user set name = #{name} where id = #{id}')
|
||||
|
||||
// 删除
|
||||
db.update('delete from user where id = #{id}')
|
||||
```
|
||||
|
||||
### 多行 SQL + 参数绑定
|
||||
```magicscript
|
||||
return db.page("""
|
||||
select u.id, u.name, u.age, d.dept_name
|
||||
from user u
|
||||
left join department d on d.id = u.dept_id
|
||||
where u.status = 1
|
||||
order by u.create_time desc
|
||||
""")
|
||||
```
|
||||
|
||||
### 数组参数自动展开
|
||||
```magicscript
|
||||
var ids = [1, 2, 3]
|
||||
return db.select('select * from user where id in (#{ids})')
|
||||
```
|
||||
|
||||
### 条件片段 ?{} 语法
|
||||
```magicscript
|
||||
// ?{condition, sql} — 条件为真时拼接 SQL 片段
|
||||
return db.select('select * from user where 1=1 ?{name, and name like concat(\'%\',#{name},\'%\')} ?{status, and status = #{status}}')
|
||||
```
|
||||
|
||||
### 命名风格转换
|
||||
```magicscript
|
||||
// 数据库下划线 → 驼峰命名
|
||||
return db.camel().select('select user_name, create_time from user')
|
||||
// 返回 [{userName: '...', createTime: '...'}]
|
||||
```
|
||||
@@ -0,0 +1,104 @@
|
||||
## Fluent Table API(db.table)
|
||||
|
||||
db.table() 提供链式操作数据库表,无需手写 SQL。
|
||||
|
||||
### 查询
|
||||
```magicscript
|
||||
// 查询所有
|
||||
return db.table('user').select()
|
||||
|
||||
// 指定列
|
||||
return db.table('user').columns('id', 'name', 'age').select()
|
||||
|
||||
// 排除列
|
||||
return db.table('user').exclude('password').select()
|
||||
|
||||
// 条件查询
|
||||
return db.table('user')
|
||||
.where()
|
||||
.eq('status', 1)
|
||||
.like('name', '%张%')
|
||||
.select()
|
||||
|
||||
// 分页
|
||||
return db.table('user').where().eq('status', 1).page()
|
||||
|
||||
// 查询单条
|
||||
return db.table('user').where().eq('id', id).selectOne()
|
||||
|
||||
// 计数
|
||||
return db.table('user').where().eq('status', 1).count()
|
||||
|
||||
// 是否存在
|
||||
return db.table('user').where().eq('name', name).exists()
|
||||
```
|
||||
|
||||
### 条件方法
|
||||
```magicscript
|
||||
.where()
|
||||
.eq('status', 1) // = 等于
|
||||
.ne('status', 0) // != 不等于
|
||||
.gt('age', 18) // > 大于
|
||||
.ge('age', 18) // >= 大于等于
|
||||
.lt('age', 60) // < 小于
|
||||
.le('age', 60) // <= 小于等于
|
||||
.like('name', '%关键字%') // LIKE 模糊
|
||||
.in('id', [1, 2, 3]) // IN 列表
|
||||
.isNull('deleted_at') // IS NULL
|
||||
.isNotNull('name') // IS NOT NULL
|
||||
.and() // AND 分组
|
||||
.or() // OR 分组
|
||||
```
|
||||
|
||||
### 排序与分组
|
||||
```magicscript
|
||||
return db.table('user')
|
||||
.where().eq('status', 1)
|
||||
.groupBy('dept_id')
|
||||
.orderBy('create_time', 'desc')
|
||||
.select()
|
||||
```
|
||||
|
||||
### 新增
|
||||
```magicscript
|
||||
// 插入(自增主键,不需要指定生成策略)
|
||||
return db.table('user')
|
||||
.primary('id')
|
||||
.insert({name: '张三', age: 25, status: 1})
|
||||
|
||||
// 插入(UUID 主键,需要用 () => uuid() 生成)
|
||||
return db.table('user')
|
||||
.primary('id', () => uuid())
|
||||
.insert({name: '张三', age: 25, status: 1})
|
||||
```
|
||||
|
||||
### 修改
|
||||
```magicscript
|
||||
return db.table('user')
|
||||
.primary('id')
|
||||
.update({id: id, name: '李四', age: 30})
|
||||
```
|
||||
|
||||
### 保存(有则更新,无则插入)
|
||||
```magicscript
|
||||
return db.table('user')
|
||||
.primary('id', () => uuid())
|
||||
.save(body)
|
||||
```
|
||||
|
||||
### 删除
|
||||
```magicscript
|
||||
return db.table('user').where().eq('id', id).delete()
|
||||
```
|
||||
|
||||
### 批量插入
|
||||
```magicscript
|
||||
var users = [{name: '张三', age: 20}, {name: '李四', age: 25}]
|
||||
return db.table('user').primary('id', () => uuid()).batchInsert(users)
|
||||
```
|
||||
|
||||
### 逻辑删除
|
||||
```magicscript
|
||||
// logic() 将 delete 转为 update deleted_flag
|
||||
return db.table('user').logic().where().eq('id', id).delete()
|
||||
```
|
||||
@@ -0,0 +1,101 @@
|
||||
## 动态 SQL(MyBatis 风格)
|
||||
|
||||
在三引号 """...""" SQL 中使用 XML 标签实现动态条件。
|
||||
|
||||
### if 标签
|
||||
```magicscript
|
||||
return db.select("""
|
||||
select * from user
|
||||
<where>
|
||||
<if test="name != null and name != ''">
|
||||
and name like concat('%', #{name}, '%')
|
||||
</if>
|
||||
<if test="status != null">
|
||||
and status = #{status}
|
||||
</if>
|
||||
<if test="minAge != null">
|
||||
and age >= #{minAge}
|
||||
</if>
|
||||
</where>
|
||||
order by create_time desc
|
||||
""")
|
||||
```
|
||||
|
||||
### if / elseif / else
|
||||
```magicscript
|
||||
return db.select("""
|
||||
select * from user where
|
||||
<if test="type == 'vip'">
|
||||
level >= 3
|
||||
</if>
|
||||
<elseif test="type == 'normal'">
|
||||
level >= 1
|
||||
</elseif>
|
||||
<else>
|
||||
1 = 1
|
||||
</else>
|
||||
""")
|
||||
```
|
||||
|
||||
### where 标签
|
||||
自动处理第一个 AND/OR,避免 WHERE AND 语法错误:
|
||||
```magicscript
|
||||
return db.select("""
|
||||
select * from user
|
||||
<where>
|
||||
<if test="name != null">and name = #{name}</if>
|
||||
<if test="age != null">and age = #{age}</if>
|
||||
</where>
|
||||
""")
|
||||
```
|
||||
|
||||
### set 标签(更新)
|
||||
自动处理末尾多余逗号:
|
||||
```magicscript
|
||||
return db.update("""
|
||||
update user
|
||||
<set>
|
||||
<if test="name != null">name = #{name},</if>
|
||||
<if test="age != null">age = #{age},</if>
|
||||
<if test="status != null">status = #{status},</if>
|
||||
</set>
|
||||
where id = #{id}
|
||||
""")
|
||||
```
|
||||
|
||||
### foreach 标签(批量)
|
||||
```magicscript
|
||||
var idList = [1, 2, 3, 4, 5]
|
||||
return db.select("""
|
||||
select * from user
|
||||
where id in
|
||||
<foreach collection="idList" item="item" open="(" separator="," close=")">
|
||||
#{item}
|
||||
</foreach>
|
||||
""")
|
||||
```
|
||||
|
||||
### 综合示例:条件搜索 + 分页
|
||||
```magicscript
|
||||
return db.page("""
|
||||
select u.*, d.dept_name
|
||||
from user u
|
||||
left join department d on d.id = u.dept_id
|
||||
<where>
|
||||
<if test="keyword != null and keyword != ''">
|
||||
and (u.name like concat('%',#{keyword},'%')
|
||||
or u.phone like concat('%',#{keyword},'%'))
|
||||
</if>
|
||||
<if test="deptId != null">
|
||||
and u.dept_id = #{deptId}
|
||||
</if>
|
||||
<if test="startDate != null">
|
||||
and u.create_time >= #{startDate}
|
||||
</if>
|
||||
<if test="endDate != null">
|
||||
and u.create_time <= #{endDate}
|
||||
</if>
|
||||
</where>
|
||||
order by u.create_time desc
|
||||
""")
|
||||
```
|
||||
@@ -0,0 +1,66 @@
|
||||
## HTTP 客户端(调用外部接口)
|
||||
|
||||
```magicscript
|
||||
import http;
|
||||
```
|
||||
|
||||
### GET 请求
|
||||
```magicscript
|
||||
// 简单 GET
|
||||
var result = http.connect('https://api.example.com/users')
|
||||
.get()
|
||||
.getBody()
|
||||
|
||||
// 带参数和请求头
|
||||
var result = http.connect('https://api.example.com/users')
|
||||
.header('Authorization', 'Bearer ' + token)
|
||||
.param('page', 1)
|
||||
.param('size', 10)
|
||||
.get()
|
||||
.getBody()
|
||||
```
|
||||
|
||||
### POST 请求
|
||||
```magicscript
|
||||
// POST JSON
|
||||
var result = http.connect('https://api.example.com/users')
|
||||
.contentType('application/json')
|
||||
.header('Authorization', 'Bearer ' + token)
|
||||
.body({name: '张三', age: 25})
|
||||
.post()
|
||||
.getBody()
|
||||
|
||||
// POST 表单
|
||||
var result = http.connect('https://api.example.com/login')
|
||||
.contentType('application/x-www-form-urlencoded')
|
||||
.data('username', 'admin')
|
||||
.data('password', '123456')
|
||||
.post()
|
||||
.getBody()
|
||||
```
|
||||
|
||||
### 其他方法
|
||||
```magicscript
|
||||
// PUT
|
||||
http.connect(url).body(data).put().getBody()
|
||||
|
||||
// DELETE
|
||||
http.connect(url).delete().getBody()
|
||||
```
|
||||
|
||||
### 综合示例:调用第三方API并处理结果
|
||||
```magicscript
|
||||
import http;
|
||||
|
||||
var apiResult = http.connect('https://api.example.com/data')
|
||||
.header('Authorization', 'Bearer ' + apiKey)
|
||||
.param('keyword', keyword)
|
||||
.get()
|
||||
.getBody()
|
||||
|
||||
if (apiResult.code != 200) {
|
||||
exit 500, '第三方接口调用失败: ' + apiResult.message
|
||||
}
|
||||
|
||||
return apiResult.data
|
||||
```
|
||||
@@ -0,0 +1,88 @@
|
||||
## Java 互操作
|
||||
|
||||
### 导入 Java 类
|
||||
```magicscript
|
||||
import 'java.util.Date' as Date
|
||||
import 'java.text.SimpleDateFormat' as SimpleDateFormat
|
||||
import 'java.util.UUID' as UUID
|
||||
|
||||
var now = new Date()
|
||||
var fmt = new SimpleDateFormat('yyyy-MM-dd HH:mm:ss')
|
||||
return fmt.format(now)
|
||||
```
|
||||
|
||||
### 导入 Spring Bean
|
||||
```magicscript
|
||||
// 从 Spring 容器获取 Bean(按类型)
|
||||
import 'org.springframework.core.env.Environment' as springEnv
|
||||
return springEnv.getProperty('server.port')
|
||||
|
||||
// 简写方式(按名称从 Spring 容器获取)
|
||||
// 需在 magic-api 中注册
|
||||
```
|
||||
|
||||
### 类型转换
|
||||
```magicscript
|
||||
// :: 操作符(null 安全)
|
||||
var num = '123'::int // String → int
|
||||
var str = 123::string // int → String
|
||||
var d = '2024-01-01'::date('yyyy-MM-dd') // String → Date
|
||||
var n = '3.14'::double // String → double
|
||||
var l = '999'::long // String → long
|
||||
|
||||
// 带默认值
|
||||
var num = str::int(0) // 转换失败返回 0
|
||||
|
||||
// JSON 转换
|
||||
var obj = '{"name":"test"}'::json // JSON 字符串 → 对象
|
||||
var jsonStr = {name: 'test'}::stringify // 对象 → JSON 字符串
|
||||
|
||||
// 扩展方法
|
||||
value.asString()
|
||||
value.asInt()
|
||||
value.asDouble()
|
||||
```
|
||||
|
||||
### 调用 Java 静态方法
|
||||
```magicscript
|
||||
import 'java.lang.Math' as Math
|
||||
import 'java.util.Collections' as Collections
|
||||
|
||||
var max = Math.max(10, 20)
|
||||
```
|
||||
|
||||
### 实用示例:生成验证码图片
|
||||
```magicscript
|
||||
import 'java.awt.image.BufferedImage' as BufferedImage
|
||||
import 'java.awt.Color' as Color
|
||||
import 'java.awt.Font' as Font
|
||||
import 'javax.imageio.ImageIO' as ImageIO
|
||||
import 'java.io.ByteArrayOutputStream' as ByteArrayOutputStream
|
||||
import response;
|
||||
|
||||
var width = 120
|
||||
var height = 40
|
||||
var image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB)
|
||||
var g = image.getGraphics()
|
||||
g.setColor(Color.WHITE)
|
||||
g.fillRect(0, 0, width, height)
|
||||
g.setFont(new Font('Arial', Font.BOLD, 24))
|
||||
g.setColor(Color.BLACK)
|
||||
g.drawString('ABCD', 10, 30)
|
||||
g.dispose()
|
||||
|
||||
var baos = new ByteArrayOutputStream()
|
||||
ImageIO.write(image, 'png', baos)
|
||||
response.image(baos.toByteArray(), 'image/png')
|
||||
```
|
||||
|
||||
### 导入自定义函数
|
||||
```magicscript
|
||||
// 调用其他 magic-api 函数
|
||||
import '@/utils/add' as add
|
||||
return add(1, 2)
|
||||
|
||||
// 调用其他 API 接口
|
||||
import '@get:/api/user/list' as getUserList
|
||||
return getUserList()
|
||||
```
|
||||
@@ -0,0 +1,91 @@
|
||||
## LINQ 查询语法
|
||||
|
||||
magic-script 支持类 SQL 的 LINQ 语法,对内存集合进行查询,可与数据库结果配合使用。
|
||||
|
||||
### 基本查询
|
||||
```magicscript
|
||||
var list = [{name: '张三', age: 20, sex: 0}, {name: '李四', age: 30, sex: 1}]
|
||||
|
||||
// 过滤
|
||||
return select * from list t where t.age > 18
|
||||
|
||||
// 选择字段 + 转换
|
||||
return select t.name, t.age > 18 ? '成年' : '未成年' status from list t
|
||||
```
|
||||
|
||||
### 分组聚合
|
||||
```magicscript
|
||||
var data = db.select('select dept_id, salary from employee')
|
||||
|
||||
return
|
||||
select
|
||||
t.dept_id,
|
||||
count(t.dept_id) total,
|
||||
sum(t.salary) totalSalary,
|
||||
avg(t.salary) avgSalary
|
||||
from data t
|
||||
group by t.dept_id
|
||||
order by sum(t.salary) desc
|
||||
```
|
||||
|
||||
### JOIN 关联
|
||||
```magicscript
|
||||
// 注意:LINQ 中不用 as 别名,直接用空格分隔
|
||||
// 使用 db.camel() 后,下划线列名会转为驼峰(如 dept_id -> deptId)
|
||||
var users = db.camel().select('select name, dept_id from user')
|
||||
var depts = db.camel().select('select id, name from department')
|
||||
|
||||
// 简单写法,不用 as 别名
|
||||
return select u.name, d.name from users u left join depts d on u.deptId = d.id
|
||||
```
|
||||
|
||||
### 空值处理
|
||||
```magicscript
|
||||
var list = [{name: null, age: 18}, {name: '张三', age: null}]
|
||||
|
||||
return select
|
||||
t.name || '未知' name, // || 将 null/空字符串 替换
|
||||
ifnull(t.age, 0) age // ifnull 仅处理 null
|
||||
from list t
|
||||
```
|
||||
|
||||
### LINQ + 分组聚合 + 排序
|
||||
```magicscript
|
||||
var orders = db.select('select * from orders')
|
||||
|
||||
return
|
||||
select
|
||||
t.category,
|
||||
count(t.id) orderCount,
|
||||
sum(t.amount) totalAmount
|
||||
from orders t
|
||||
group by t.category
|
||||
having sum(t.amount) > 1000
|
||||
order by totalAmount desc
|
||||
```
|
||||
|
||||
### 嵌套查询 + 树形结构
|
||||
```magicscript
|
||||
var allDepts = db.select('select id, name, parent_id from department')
|
||||
|
||||
var toTree = (list, parentId) =>
|
||||
select t.*, toTree(list, t.id) children
|
||||
from list t
|
||||
where t.parent_id = parentId
|
||||
|
||||
return toTree(allDepts, '0')
|
||||
```
|
||||
|
||||
### 子查询
|
||||
```magicscript
|
||||
return
|
||||
select * from (
|
||||
select
|
||||
t.dept_id,
|
||||
count(t.id) cnt
|
||||
from users t
|
||||
group by t.dept_id
|
||||
) sub
|
||||
where sub.cnt > 5
|
||||
order by sub.cnt desc
|
||||
```
|
||||
@@ -0,0 +1,108 @@
|
||||
## 请求与响应处理
|
||||
|
||||
### 获取请求参数
|
||||
```magicscript
|
||||
// 路径变量(API 路径定义为 /user/{id})
|
||||
var userId = id
|
||||
|
||||
// Query 参数(GET 请求 ?name=xxx)
|
||||
var userName = name
|
||||
|
||||
// 请求体(POST JSON)
|
||||
var data = body // 整个请求体对象
|
||||
var userName = body.name // 请求体字段
|
||||
|
||||
// 请求头
|
||||
var token = header.token
|
||||
var auth = header.Authorization
|
||||
|
||||
// 路径变量(显式)
|
||||
var userId = path.id
|
||||
```
|
||||
|
||||
### request 对象
|
||||
```magicscript
|
||||
import request;
|
||||
|
||||
// 获取请求头
|
||||
request.getHeader('Authorization')
|
||||
request.getHeaders('Accept') // 返回数组
|
||||
|
||||
// 获取参数
|
||||
request.getParameter('name')
|
||||
|
||||
// 获取客户端IP
|
||||
request.getClientIP()
|
||||
|
||||
// 获取上传文件
|
||||
var file = request.getFile('file')
|
||||
var fileName = file.getOriginalFilename()
|
||||
var fileBytes = file.getBytes()
|
||||
```
|
||||
|
||||
### response 对象
|
||||
```magicscript
|
||||
import response;
|
||||
|
||||
// 返回 JSON(跳过框架默认包装)
|
||||
response.json({code: 200, data: result})
|
||||
|
||||
// 返回纯文本
|
||||
response.text('ok')
|
||||
|
||||
// 设置响应头
|
||||
response.setHeader('X-Custom', 'value')
|
||||
|
||||
// 设置 Cookie
|
||||
response.addCookie('token', jwtToken)
|
||||
|
||||
// 文件下载
|
||||
response.download(fileBytes, '报表.xlsx')
|
||||
|
||||
// 返回图片
|
||||
response.image(imageBytes, 'image/png')
|
||||
|
||||
// 分页结果(自定义格式)
|
||||
response.page(total, list)
|
||||
```
|
||||
|
||||
### exit 快速返回
|
||||
```magicscript
|
||||
// 参数校验失败时快速返回
|
||||
if (!name || name.trim() == '') {
|
||||
exit 400, '名称不能为空'
|
||||
}
|
||||
if (age::int < 0 || age::int > 150) {
|
||||
exit 400, '年龄不合法'
|
||||
}
|
||||
|
||||
// 带数据返回
|
||||
exit 200, '操作成功', {id: newId}
|
||||
```
|
||||
|
||||
### 完整 CRUD 示例
|
||||
```magicscript
|
||||
// POST /api/user — 新增用户(自增主键)
|
||||
if (!body.name || body.name == '') {
|
||||
exit 400, '用户名不能为空'
|
||||
}
|
||||
return db.table('user')
|
||||
.primary('id')
|
||||
.insert(body)
|
||||
```
|
||||
|
||||
### 修改记录示例
|
||||
```magicscript
|
||||
// POST /api/user/update — 修改用户
|
||||
if (!body.id) {
|
||||
exit 400, 'ID不能为空'
|
||||
}
|
||||
// 过滤掉id字段,构建新map(map不能用delete)
|
||||
var updateData = body.filter((k, v) => k != 'id')
|
||||
return db.table('user')
|
||||
.where()
|
||||
.eq('id', body.id::int)
|
||||
.set('name', updateData.name)
|
||||
.set('status', updateData.status)
|
||||
.update()
|
||||
```
|
||||
@@ -0,0 +1,51 @@
|
||||
## 事务与缓存
|
||||
|
||||
### 自动事务
|
||||
```magicscript
|
||||
// lambda 内的所有数据库操作在同一事务中,异常自动回滚
|
||||
var result = db.transaction(() => {
|
||||
db.update('update account set balance = balance - #{amount} where id = #{fromId}')
|
||||
db.update('update account set balance = balance + #{amount} where id = #{toId}')
|
||||
return '转账成功'
|
||||
})
|
||||
return result
|
||||
```
|
||||
|
||||
### 手动事务
|
||||
```magicscript
|
||||
var tx = db.transaction()
|
||||
try {
|
||||
db.insert('insert into order_log(order_id, action) values(#{orderId}, "create")')
|
||||
db.update('update product set stock = stock - #{qty} where id = #{productId}')
|
||||
tx.commit()
|
||||
return '操作成功'
|
||||
} catch(e) {
|
||||
tx.rollback()
|
||||
exit 500, '操作失败: ' + e.getMessage()
|
||||
}
|
||||
```
|
||||
|
||||
### 缓存查询
|
||||
```magicscript
|
||||
// 使用缓存,key='user',有效期 60000ms(60秒)
|
||||
return db.cache('user', 60000).select('select * from user')
|
||||
|
||||
// 更新时自动清除对应缓存
|
||||
db.cache('user').update('update user set name = #{name} where id = #{id}')
|
||||
|
||||
// 手动清除缓存
|
||||
db.deleteCache('user')
|
||||
```
|
||||
|
||||
### 多数据源
|
||||
```magicscript
|
||||
// 使用从库查询
|
||||
return db.slave.select('select * from user')
|
||||
|
||||
// 动态数据源(变量指定)
|
||||
var dsKey = 'report_db'
|
||||
return db[dsKey].select('select * from statistics')
|
||||
|
||||
// 不同数据源支持同样的 API(select/table 等)
|
||||
return db.slave.table('user').where().eq('status', 1).select()
|
||||
```
|
||||
Reference in New Issue
Block a user