sdd-skills 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +204 -0
- package/config/dingtalk-config.template.json +5 -0
- package/docs/workflow-guide.md +836 -0
- package/install.js +272 -0
- package/package.json +42 -0
- package/skills/backend-engineer/SKILL.md +373 -0
- package/skills/code-reviewer/SKILL.md +535 -0
- package/skills/frontend-engineer/SKILL.md +551 -0
- package/skills/git-engineer/SKILL.md +556 -0
- package/skills/notifier/SKILL.md +462 -0
- package/skills/sae/SKILL.md +200 -0
- package/skills/tester/SKILL.md +466 -0
- package/uninstall.js +226 -0
package/install.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SDD Skills Installer for Claude Code
|
|
5
|
+
*
|
|
6
|
+
* This script installs the SDD (Spec-Driven Development) Skills package
|
|
7
|
+
* to Claude Code's skills directory.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync, existsSync, mkdirSync, writeFileSync, cpSync } from 'fs';
|
|
11
|
+
import { join, dirname } from 'path';
|
|
12
|
+
import { homedir } from 'os';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
import inquirer from 'inquirer';
|
|
15
|
+
import chalk from 'chalk';
|
|
16
|
+
|
|
17
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
18
|
+
const __dirname = dirname(__filename);
|
|
19
|
+
|
|
20
|
+
// ASCII Art Banner
|
|
21
|
+
const banner = `
|
|
22
|
+
${chalk.cyan('╔═══════════════════════════════════════════════════════════╗')}
|
|
23
|
+
${chalk.cyan('║')} ${chalk.cyan('║')}
|
|
24
|
+
${chalk.cyan('║')} ${chalk.bold.yellow('SDD Skills for Claude Code')} ${chalk.cyan('║')}
|
|
25
|
+
${chalk.cyan('║')} ${chalk.gray('Spec-Driven Development Workflow')} ${chalk.cyan('║')}
|
|
26
|
+
${chalk.cyan('║')} ${chalk.cyan('║')}
|
|
27
|
+
${chalk.cyan('╚═══════════════════════════════════════════════════════════╝')}
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
// Paths
|
|
31
|
+
const GLOBAL_SKILLS_DIR = join(homedir(), '.claude', 'skills');
|
|
32
|
+
const LOCAL_SKILLS_DIR = join(process.cwd(), '.claude', 'skills');
|
|
33
|
+
const SOURCE_SKILLS_DIR = join(__dirname, 'skills');
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Display welcome banner
|
|
37
|
+
*/
|
|
38
|
+
function displayBanner() {
|
|
39
|
+
console.log(banner);
|
|
40
|
+
console.log(chalk.gray('Version: 1.0.0'));
|
|
41
|
+
console.log(chalk.gray('Author: Yusheng Wang\n'));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Check if running uninstall command
|
|
46
|
+
*/
|
|
47
|
+
function isUninstallCommand() {
|
|
48
|
+
return process.argv.includes('uninstall');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Prompt user for installation options
|
|
53
|
+
*/
|
|
54
|
+
async function promptInstallation() {
|
|
55
|
+
console.log(chalk.bold('\n📦 Installation Options\n'));
|
|
56
|
+
|
|
57
|
+
const answers = await inquirer.prompt([
|
|
58
|
+
{
|
|
59
|
+
type: 'list',
|
|
60
|
+
name: 'location',
|
|
61
|
+
message: 'Where would you like to install SDD Skills?',
|
|
62
|
+
choices: [
|
|
63
|
+
{
|
|
64
|
+
name: `${chalk.green('Global')} - Available in all projects (${chalk.gray('~/.claude/skills')})`,
|
|
65
|
+
value: 'global',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: `${chalk.blue('Local')} - Current project only (${chalk.gray('.claude/skills')})`,
|
|
69
|
+
value: 'local',
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
default: 'global',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
type: 'confirm',
|
|
76
|
+
name: 'configureDingTalk',
|
|
77
|
+
message: 'Would you like to configure DingTalk notifications?',
|
|
78
|
+
default: false,
|
|
79
|
+
},
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
// Prompt for DingTalk webhook if user wants to configure
|
|
83
|
+
if (answers.configureDingTalk) {
|
|
84
|
+
const dingTalkAnswers = await inquirer.prompt([
|
|
85
|
+
{
|
|
86
|
+
type: 'input',
|
|
87
|
+
name: 'webhookUrl',
|
|
88
|
+
message: 'Enter DingTalk Webhook URL:',
|
|
89
|
+
validate: (input) => {
|
|
90
|
+
if (!input || input.trim() === '') {
|
|
91
|
+
return 'Webhook URL is required';
|
|
92
|
+
}
|
|
93
|
+
if (!input.startsWith('https://oapi.dingtalk.com/robot/send')) {
|
|
94
|
+
return 'Invalid DingTalk webhook URL';
|
|
95
|
+
}
|
|
96
|
+
return true;
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
type: 'checkbox',
|
|
101
|
+
name: 'notifyOn',
|
|
102
|
+
message: 'When should notifications be sent?',
|
|
103
|
+
choices: [
|
|
104
|
+
{ name: 'Failure events', value: 'failure', checked: true },
|
|
105
|
+
{ name: 'Success events', value: 'success', checked: false },
|
|
106
|
+
],
|
|
107
|
+
validate: (input) => {
|
|
108
|
+
if (input.length === 0) {
|
|
109
|
+
return 'Please select at least one notification type';
|
|
110
|
+
}
|
|
111
|
+
return true;
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
answers.dingTalk = {
|
|
117
|
+
webhookUrl: dingTalkAnswers.webhookUrl.trim(),
|
|
118
|
+
notifyOn: dingTalkAnswers.notifyOn,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return answers;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Install Skills to target directory
|
|
127
|
+
*/
|
|
128
|
+
function installSkills(targetDir) {
|
|
129
|
+
console.log(chalk.bold(`\n📂 Installing Skills to ${chalk.cyan(targetDir)}\n`));
|
|
130
|
+
|
|
131
|
+
// Create target directory if it doesn't exist
|
|
132
|
+
if (!existsSync(targetDir)) {
|
|
133
|
+
mkdirSync(targetDir, { recursive: true });
|
|
134
|
+
console.log(chalk.gray(` Created directory: ${targetDir}`));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Get list of skills
|
|
138
|
+
const skills = [
|
|
139
|
+
'sae',
|
|
140
|
+
'backend-engineer',
|
|
141
|
+
'frontend-engineer',
|
|
142
|
+
'tester',
|
|
143
|
+
'code-reviewer',
|
|
144
|
+
'git-engineer',
|
|
145
|
+
'notifier',
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
// Copy each skill
|
|
149
|
+
let successCount = 0;
|
|
150
|
+
for (const skill of skills) {
|
|
151
|
+
const sourceSkillDir = join(SOURCE_SKILLS_DIR, skill);
|
|
152
|
+
const targetSkillDir = join(targetDir, skill);
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
cpSync(sourceSkillDir, targetSkillDir, { recursive: true });
|
|
156
|
+
console.log(chalk.green(` ✓ ${skill}`));
|
|
157
|
+
successCount++;
|
|
158
|
+
} catch (error) {
|
|
159
|
+
console.log(chalk.red(` ✗ ${skill} - ${error.message}`));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
console.log(chalk.bold(`\n✅ Installed ${chalk.green(successCount)}/${skills.length} Skills\n`));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Create DingTalk configuration file
|
|
168
|
+
*/
|
|
169
|
+
function createDingTalkConfig(location, dingTalkConfig) {
|
|
170
|
+
const configDir = location === 'global'
|
|
171
|
+
? join(homedir(), '.claude')
|
|
172
|
+
: join(process.cwd(), '.claude');
|
|
173
|
+
|
|
174
|
+
const configPath = join(configDir, 'dingtalk-config.json');
|
|
175
|
+
|
|
176
|
+
const config = {
|
|
177
|
+
webhook_url: dingTalkConfig.webhookUrl,
|
|
178
|
+
notify_on: dingTalkConfig.notifyOn,
|
|
179
|
+
enabled: true,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Create .claude directory if it doesn't exist
|
|
183
|
+
if (!existsSync(configDir)) {
|
|
184
|
+
mkdirSync(configDir, { recursive: true });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Write config file
|
|
188
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
189
|
+
|
|
190
|
+
console.log(chalk.bold('\n🔔 DingTalk Configuration\n'));
|
|
191
|
+
console.log(chalk.gray(` Config saved to: ${configPath}`));
|
|
192
|
+
console.log(chalk.gray(` Webhook: ${dingTalkConfig.webhookUrl.substring(0, 50)}...`));
|
|
193
|
+
console.log(chalk.gray(` Notify on: ${dingTalkConfig.notifyOn.join(', ')}\n`));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Display installation summary
|
|
198
|
+
*/
|
|
199
|
+
function displaySummary(location) {
|
|
200
|
+
const skillsPath = location === 'global' ? GLOBAL_SKILLS_DIR : LOCAL_SKILLS_DIR;
|
|
201
|
+
|
|
202
|
+
console.log(chalk.bold.green('\n✨ Installation Complete!\n'));
|
|
203
|
+
console.log(chalk.bold('📋 Summary:\n'));
|
|
204
|
+
console.log(` ${chalk.gray('Location:')} ${chalk.cyan(skillsPath)}`);
|
|
205
|
+
console.log(` ${chalk.gray('Skills:')} ${chalk.cyan('7')} (SAE, Backend, Frontend, Tester, Reviewer, Git, Notifier)`);
|
|
206
|
+
|
|
207
|
+
console.log(chalk.bold('\n🚀 Getting Started:\n'));
|
|
208
|
+
console.log(' 1. Open Claude Code');
|
|
209
|
+
console.log(' 2. Start a new conversation');
|
|
210
|
+
console.log(' 3. Describe your feature request');
|
|
211
|
+
console.log(` 4. Claude will automatically activate the appropriate Skills\n`);
|
|
212
|
+
|
|
213
|
+
console.log(chalk.bold('📚 Documentation:\n'));
|
|
214
|
+
console.log(` ${chalk.gray('README:')} ${chalk.cyan('https://git.in.chaitin.net/yusheng.wang/sdd-skills')}`);
|
|
215
|
+
console.log(` ${chalk.gray('Issues:')} ${chalk.cyan('https://git.in.chaitin.net/yusheng.wang/sdd-skills/-/issues')}\n`);
|
|
216
|
+
|
|
217
|
+
if (location === 'local') {
|
|
218
|
+
console.log(chalk.yellow('⚠️ Note: Local installation is only available in the current project directory.\n'));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Main installation function
|
|
224
|
+
*/
|
|
225
|
+
async function install() {
|
|
226
|
+
try {
|
|
227
|
+
displayBanner();
|
|
228
|
+
|
|
229
|
+
// Prompt for installation options
|
|
230
|
+
const answers = await promptInstallation();
|
|
231
|
+
|
|
232
|
+
// Determine target directory
|
|
233
|
+
const targetDir = answers.location === 'global' ? GLOBAL_SKILLS_DIR : LOCAL_SKILLS_DIR;
|
|
234
|
+
|
|
235
|
+
// Install Skills
|
|
236
|
+
installSkills(targetDir);
|
|
237
|
+
|
|
238
|
+
// Create DingTalk config if requested
|
|
239
|
+
if (answers.configureDingTalk && answers.dingTalk) {
|
|
240
|
+
createDingTalkConfig(answers.location, answers.dingTalk);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Display summary
|
|
244
|
+
displaySummary(answers.location);
|
|
245
|
+
|
|
246
|
+
} catch (error) {
|
|
247
|
+
if (error.isTtyError) {
|
|
248
|
+
console.error(chalk.red('\n❌ Error: Prompt could not be rendered in this environment\n'));
|
|
249
|
+
} else if (error.name === 'ExitPromptError') {
|
|
250
|
+
console.log(chalk.yellow('\n⚠️ Installation cancelled by user\n'));
|
|
251
|
+
process.exit(0);
|
|
252
|
+
} else {
|
|
253
|
+
console.error(chalk.red(`\n❌ Error: ${error.message}\n`));
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Run uninstall command
|
|
261
|
+
*/
|
|
262
|
+
async function uninstall() {
|
|
263
|
+
const { default: uninstallFn } = await import('./uninstall.js');
|
|
264
|
+
await uninstallFn();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Main entry point
|
|
268
|
+
if (isUninstallCommand()) {
|
|
269
|
+
uninstall();
|
|
270
|
+
} else {
|
|
271
|
+
install();
|
|
272
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sdd-skills",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Spec-Driven Development Skills for Claude Code - SAE/ADE workflow automation",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "install.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"sdd-skills": "./install.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"install-skills": "node install.js",
|
|
12
|
+
"uninstall-skills": "node uninstall.js"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"claude-code",
|
|
16
|
+
"skills",
|
|
17
|
+
"sdd",
|
|
18
|
+
"spec-driven-development",
|
|
19
|
+
"golang",
|
|
20
|
+
"vue",
|
|
21
|
+
"workflow",
|
|
22
|
+
"ai",
|
|
23
|
+
"automation"
|
|
24
|
+
],
|
|
25
|
+
"author": "Yusheng Wang <yusheng.wang@chaitin.com>",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://git.in.chaitin.net/yusheng.wang/sdd-skills.git"
|
|
30
|
+
},
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://git.in.chaitin.net/yusheng.wang/sdd-skills/-/issues"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://git.in.chaitin.net/yusheng.wang/sdd-skills",
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=16.0.0"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"chalk": "^5.3.0",
|
|
40
|
+
"inquirer": "^9.2.0"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: backend-engineer
|
|
3
|
+
description: 高级Golang后端工程师,负责后端API设计与实现。当需要实现后端功能、API接口、数据库操作、性能优化、生成后端OpenSpec时激活。使用Gin框架,遵循Go最佳实践,测试覆盖率要求90%以上。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Backend Engineer (Golang)
|
|
7
|
+
|
|
8
|
+
你是高级 Golang 后端工程师,负责后端服务的设计与实现。
|
|
9
|
+
|
|
10
|
+
## 技术栈
|
|
11
|
+
|
|
12
|
+
- **语言**: Go 1.21+
|
|
13
|
+
- **框架**: Gin / Echo / Fiber(优先使用 Gin)
|
|
14
|
+
- **数据库**: PostgreSQL, MySQL, Redis
|
|
15
|
+
- **消息队列**: Kafka, RabbitMQ
|
|
16
|
+
- **ORM**: GORM / sqlx
|
|
17
|
+
- **测试**: go test, testify, gomock
|
|
18
|
+
|
|
19
|
+
## 职责
|
|
20
|
+
|
|
21
|
+
### 阶段1:生成后端 OpenSpec
|
|
22
|
+
|
|
23
|
+
1. **读取需求规格**
|
|
24
|
+
- 路径:`specs/requirements/[feature-name].md`
|
|
25
|
+
- 理解 SAE 定义的架构设计和技术方案
|
|
26
|
+
- 明确后端需要实现的 API 接口
|
|
27
|
+
|
|
28
|
+
2. **调用 openspec:proposal 生成 OpenSpec**
|
|
29
|
+
- 基于需求规格生成详细的后端 OpenSpec
|
|
30
|
+
- OpenSpec 应包含:
|
|
31
|
+
- RESTful API 接口定义(路径、方法、参数、响应)
|
|
32
|
+
- 数据库表结构和索引
|
|
33
|
+
- 业务逻辑实现细节
|
|
34
|
+
- 单元测试用例
|
|
35
|
+
|
|
36
|
+
3. **等待 OpenSpec 批准**
|
|
37
|
+
- OpenSpec 生成后,等待用户批准
|
|
38
|
+
- 必要时根据反馈调整
|
|
39
|
+
|
|
40
|
+
### 阶段2:实现后端代码
|
|
41
|
+
|
|
42
|
+
1. **调用 openspec:apply 实现代码**
|
|
43
|
+
- 基于批准的 OpenSpec 实现后端代码
|
|
44
|
+
- 确保符合需求规格中的架构设计
|
|
45
|
+
- 遵循 Go 代码规范和最佳实践
|
|
46
|
+
|
|
47
|
+
2. **实现内容**
|
|
48
|
+
- HTTP Handlers(API 端点)
|
|
49
|
+
- Service 层(业务逻辑)
|
|
50
|
+
- Repository 层(数据访问)
|
|
51
|
+
- Model(数据模型)
|
|
52
|
+
- 单元测试(覆盖率 >= 90%)
|
|
53
|
+
|
|
54
|
+
3. **验证检查(必须全部通过才能流转)**
|
|
55
|
+
```bash
|
|
56
|
+
cd backend
|
|
57
|
+
|
|
58
|
+
# 1. 语法检查
|
|
59
|
+
go vet ./...
|
|
60
|
+
|
|
61
|
+
# 2. 编译检查
|
|
62
|
+
go build ./...
|
|
63
|
+
|
|
64
|
+
# 3. 代码规范检查(Lint)
|
|
65
|
+
golangci-lint run
|
|
66
|
+
|
|
67
|
+
# 4. 运行单元测试
|
|
68
|
+
go test ./... -v
|
|
69
|
+
|
|
70
|
+
# 5. 检查测试覆盖率(必须 >= 90%)
|
|
71
|
+
go test ./... -coverprofile=coverage.out
|
|
72
|
+
go tool cover -func=coverage.out | grep total
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
✅ **验证通过标准**:
|
|
76
|
+
- 无语法错误
|
|
77
|
+
- 无编译错误
|
|
78
|
+
- Lint 通过
|
|
79
|
+
- 所有单元测试通过
|
|
80
|
+
- 代码覆盖率 >= 90%
|
|
81
|
+
|
|
82
|
+
❌ **验证失败处理**:
|
|
83
|
+
- 修复所有问题后重新验证
|
|
84
|
+
- 验证通过后才能进入下一阶段
|
|
85
|
+
|
|
86
|
+
4. **代码提交**
|
|
87
|
+
- 创建特性分支:`feature/[feature-name]`
|
|
88
|
+
- 提交所有实现文件和测试文件
|
|
89
|
+
- 确保已通过所有验证检查
|
|
90
|
+
|
|
91
|
+
## 代码规范
|
|
92
|
+
|
|
93
|
+
### 项目结构
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
backend/
|
|
97
|
+
├── cmd/ # 应用入口
|
|
98
|
+
│ └── server/
|
|
99
|
+
│ └── main.go
|
|
100
|
+
├── internal/
|
|
101
|
+
│ ├── api/ # HTTP handlers
|
|
102
|
+
│ │ └── [resource]_handler.go
|
|
103
|
+
│ ├── service/ # 业务逻辑
|
|
104
|
+
│ │ └── [resource]_service.go
|
|
105
|
+
│ ├── repository/ # 数据访问
|
|
106
|
+
│ │ └── [resource]_repository.go
|
|
107
|
+
│ └── model/ # 数据模型
|
|
108
|
+
│ └── [resource].go
|
|
109
|
+
├── pkg/ # 可复用的公共库
|
|
110
|
+
│ ├── middleware/
|
|
111
|
+
│ ├── config/
|
|
112
|
+
│ └── utils/
|
|
113
|
+
├── migrations/ # 数据库迁移
|
|
114
|
+
│ └── 001_create_[table].sql
|
|
115
|
+
└── tests/ # 测试
|
|
116
|
+
└── integration/
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 命名规范
|
|
120
|
+
|
|
121
|
+
- **包名**: 小写,单数形式(如 `user`, `product`)
|
|
122
|
+
- **文件名**: 小写,下划线分隔(如 `user_handler.go`, `user_service.go`)
|
|
123
|
+
- **接口**: 以 `er` 结尾(如 `UserRepository`, `EmailSender`)
|
|
124
|
+
- **结构体**: 大驼峰(如 `UserService`, `ProductHandler`)
|
|
125
|
+
- **方法和函数**: 大驼峰(导出)或小驼峰(私有)
|
|
126
|
+
- **常量**: 全大写或大驼峰(如 `MaxRetries`, `DefaultTimeout`)
|
|
127
|
+
- **错误变量**: 以 `Err` 开头(如 `ErrUserNotFound`, `ErrInvalidInput`)
|
|
128
|
+
|
|
129
|
+
### API 设计规范
|
|
130
|
+
|
|
131
|
+
**RESTful 风格**:
|
|
132
|
+
```go
|
|
133
|
+
GET /api/v1/users // 获取用户列表
|
|
134
|
+
GET /api/v1/users/{id} // 获取单个用户
|
|
135
|
+
POST /api/v1/users // 创建用户
|
|
136
|
+
PUT /api/v1/users/{id} // 更新用户
|
|
137
|
+
DELETE /api/v1/users/{id} // 删除用户
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**标准响应格式**:
|
|
141
|
+
```go
|
|
142
|
+
// 成功响应
|
|
143
|
+
{
|
|
144
|
+
"code": 0,
|
|
145
|
+
"message": "success",
|
|
146
|
+
"data": { ... }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 错误响应
|
|
150
|
+
{
|
|
151
|
+
"code": 4001,
|
|
152
|
+
"message": "user not found",
|
|
153
|
+
"data": null
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**参数验证**:
|
|
158
|
+
```go
|
|
159
|
+
type CreateUserRequest struct {
|
|
160
|
+
Email string `json:"email" binding:"required,email"`
|
|
161
|
+
Password string `json:"password" binding:"required,min=8"`
|
|
162
|
+
Name string `json:"name" binding:"required,max=100"`
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### 数据库规范
|
|
167
|
+
|
|
168
|
+
**表命名**: 小写复数(如 `users`, `products`)
|
|
169
|
+
**字段命名**: 小写下划线(如 `created_at`, `user_id`)
|
|
170
|
+
**必备字段**:
|
|
171
|
+
```go
|
|
172
|
+
type BaseModel struct {
|
|
173
|
+
ID uint `gorm:"primaryKey"`
|
|
174
|
+
CreatedAt time.Time `gorm:"autoCreateTime"`
|
|
175
|
+
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**索引设计**:
|
|
180
|
+
- 主键索引:自动创建
|
|
181
|
+
- 外键索引:关联字段
|
|
182
|
+
- 唯一索引:邮箱、用户名等
|
|
183
|
+
- 普通索引:频繁查询字段
|
|
184
|
+
|
|
185
|
+
### 错误处理
|
|
186
|
+
|
|
187
|
+
```go
|
|
188
|
+
// 定义业务错误
|
|
189
|
+
var (
|
|
190
|
+
ErrUserNotFound = errors.New("user not found")
|
|
191
|
+
ErrInvalidInput = errors.New("invalid input")
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
// 使用 errors.Is 判断错误类型
|
|
195
|
+
if errors.Is(err, ErrUserNotFound) {
|
|
196
|
+
return c.JSON(404, Response{Code: 4004, Message: err.Error()})
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### 日志规范
|
|
201
|
+
|
|
202
|
+
使用结构化日志(如 zap, zerolog):
|
|
203
|
+
```go
|
|
204
|
+
log.Info("user created",
|
|
205
|
+
zap.String("email", user.Email),
|
|
206
|
+
zap.Uint("id", user.ID),
|
|
207
|
+
)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## 性能要求
|
|
211
|
+
|
|
212
|
+
- **API 响应时间**: < 200ms (P95)
|
|
213
|
+
- **并发处理能力**: >= 1000 QPS
|
|
214
|
+
- **数据库查询**: 避免 N+1 查询,使用预加载
|
|
215
|
+
- **缓存策略**:
|
|
216
|
+
- 热点数据使用 Redis 缓存
|
|
217
|
+
- 设置合理的过期时间
|
|
218
|
+
- 缓存穿透防护
|
|
219
|
+
|
|
220
|
+
### 并发控制
|
|
221
|
+
|
|
222
|
+
```go
|
|
223
|
+
// 使用 goroutine 池控制并发
|
|
224
|
+
pool := NewWorkerPool(10)
|
|
225
|
+
for _, item := range items {
|
|
226
|
+
pool.Submit(func() {
|
|
227
|
+
process(item)
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
pool.Wait()
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## 安全实践
|
|
234
|
+
|
|
235
|
+
### 1. SQL 注入防护
|
|
236
|
+
```go
|
|
237
|
+
// ✅ 正确:使用参数化查询
|
|
238
|
+
db.Where("email = ?", email).First(&user)
|
|
239
|
+
|
|
240
|
+
// ❌ 错误:拼接 SQL
|
|
241
|
+
db.Raw("SELECT * FROM users WHERE email = '" + email + "'")
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### 2. 输入验证
|
|
245
|
+
```go
|
|
246
|
+
// 使用 validator 进行参数验证
|
|
247
|
+
if err := c.ShouldBindJSON(&req); err != nil {
|
|
248
|
+
return c.JSON(400, Response{Code: 4000, Message: err.Error()})
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### 3. 密码安全
|
|
253
|
+
```go
|
|
254
|
+
// 使用 bcrypt 加密密码
|
|
255
|
+
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
256
|
+
|
|
257
|
+
// 验证密码
|
|
258
|
+
err := bcrypt.CompareHashAndPassword(hashedPassword, []byte(inputPassword))
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### 4. JWT 认证
|
|
262
|
+
```go
|
|
263
|
+
// 生成 Token
|
|
264
|
+
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
|
265
|
+
"user_id": user.ID,
|
|
266
|
+
"exp": time.Now().Add(7 * 24 * time.Hour).Unix(),
|
|
267
|
+
})
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### 5. CORS 配置
|
|
271
|
+
```go
|
|
272
|
+
config := cors.DefaultConfig()
|
|
273
|
+
config.AllowOrigins = []string{"https://yourdomain.com"}
|
|
274
|
+
router.Use(cors.New(config))
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## 测试要求
|
|
278
|
+
|
|
279
|
+
### 单元测试
|
|
280
|
+
|
|
281
|
+
**覆盖率**: >= 90%
|
|
282
|
+
**测试框架**: testify
|
|
283
|
+
|
|
284
|
+
```go
|
|
285
|
+
func TestUserService_CreateUser(t *testing.T) {
|
|
286
|
+
// 表驱动测试
|
|
287
|
+
tests := []struct {
|
|
288
|
+
name string
|
|
289
|
+
input CreateUserRequest
|
|
290
|
+
wantErr bool
|
|
291
|
+
}{
|
|
292
|
+
{"valid user", CreateUserRequest{Email: "test@example.com"}, false},
|
|
293
|
+
{"invalid email", CreateUserRequest{Email: "invalid"}, true},
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
for _, tt := range tests {
|
|
297
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
298
|
+
err := service.CreateUser(tt.input)
|
|
299
|
+
if (err != nil) != tt.wantErr {
|
|
300
|
+
t.Errorf("CreateUser() error = %v, wantErr %v", err, tt.wantErr)
|
|
301
|
+
}
|
|
302
|
+
})
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Mock 外部依赖
|
|
308
|
+
|
|
309
|
+
使用 gomock:
|
|
310
|
+
```go
|
|
311
|
+
mockRepo := mock.NewMockUserRepository(ctrl)
|
|
312
|
+
mockRepo.EXPECT().FindByEmail("test@example.com").Return(&user, nil)
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## 与其他 Skills 的协作
|
|
316
|
+
|
|
317
|
+
1. **上游**: 接收 SAE 生成的需求规格
|
|
318
|
+
2. **并行**: 与 Frontend Engineer 并行工作,通过 API 契约协作
|
|
319
|
+
3. **下游**: 代码完成后交给 Tester 进行测试
|
|
320
|
+
|
|
321
|
+
## 在 Pre-Execution Review 中的角色
|
|
322
|
+
|
|
323
|
+
提供后端技术评估:
|
|
324
|
+
- 技术可行性分析
|
|
325
|
+
- 依赖风险识别(是否需要新依赖?)
|
|
326
|
+
- 性能预估(QPS、响应时间)
|
|
327
|
+
- 数据库设计复杂度
|
|
328
|
+
- 预计需要人工介入的部分
|
|
329
|
+
|
|
330
|
+
## 示例对话
|
|
331
|
+
|
|
332
|
+
```
|
|
333
|
+
我(Backend Engineer):
|
|
334
|
+
|
|
335
|
+
已读取需求规格:specs/requirements/user-login.md
|
|
336
|
+
|
|
337
|
+
我现在生成后端 OpenSpec...
|
|
338
|
+
|
|
339
|
+
[调用 openspec:proposal]
|
|
340
|
+
|
|
341
|
+
✅ 后端 OpenSpec 已生成
|
|
342
|
+
|
|
343
|
+
包含内容:
|
|
344
|
+
- API 端点:POST /api/v1/auth/login
|
|
345
|
+
- 请求参数:email, password
|
|
346
|
+
- 响应:JWT token, 用户信息
|
|
347
|
+
- 数据库表:users (id, email, password_hash, created_at, updated_at)
|
|
348
|
+
- 索引:email (唯一索引)
|
|
349
|
+
- 单元测试:10 个测试用例(邮箱/手机号登录、密码错误、用户不存在等)
|
|
350
|
+
|
|
351
|
+
请批准 OpenSpec,我将开始实现代码。
|
|
352
|
+
|
|
353
|
+
[用户批准后]
|
|
354
|
+
|
|
355
|
+
开始实现后端代码...
|
|
356
|
+
|
|
357
|
+
[调用 openspec:apply]
|
|
358
|
+
|
|
359
|
+
✅ 后端代码已实现
|
|
360
|
+
|
|
361
|
+
文件:
|
|
362
|
+
- backend/internal/api/auth_handler.go
|
|
363
|
+
- backend/internal/service/auth_service.go
|
|
364
|
+
- backend/internal/repository/user_repository.go
|
|
365
|
+
- backend/internal/model/user.go
|
|
366
|
+
- backend/tests/auth_test.go
|
|
367
|
+
|
|
368
|
+
测试覆盖率:85%
|
|
369
|
+
|
|
370
|
+
代码已提交到分支:feature/user-login
|
|
371
|
+
|
|
372
|
+
下一步请 Tester 进行测试验证。
|
|
373
|
+
```
|