标签:独立开发 | AI编程 | 全栈实践 | 阿里云 | 部署上线
阅读时间:15分钟 | 代码可直接运行
前言:为什么写这篇实录
上周六早上10点,我突发奇想:能不能用48小时,从0到1上线一个完整的AI应用?
不是那种简单的Demo,而是真正能用的产品------有数据库、有用户认证、有AI能力、能部署上线、能被朋友打开用。
结果:周日晚上10点,产品上线了。朋友已经在里面创建了自己的待办清单。
这篇文章不是炫技,而是记录这48小时里,我的技术选型、开发流程、踩的坑、以及怎么把部署时间压缩到30分钟。如果你也是一个人做全栈项目,希望这篇实录能给你一套可复用的流水线。
产品需求:AI智能待办清单
核心功能:
- 用户注册/登录(JWT认证)
- 创建、编辑、删除待办事项
- AI自动优先级排序(根据任务描述+截止日期智能打分)
- AI任务拆解(输入"准备发布会",自动拆成10个子任务)
- 响应式UI,手机也能用
技术栈:
- 前端:React 18 + Tailwind CSS + Vite
- 后端:Node.js + Express + SQLite(MVP阶段不想折腾数据库部署)
- AI:OpenAI GPT-4o mini API
- 部署:阿里云OPC创业套餐(MVP阶段)
Day 1:开发(Hour 0-24)
Hour 0-2:项目初始化与架构设计
先搭目录结构:
bash
mkdir ai-todo && cd ai-todo
mkdir client server
# 前端
cd client
npm create vite@latest . -- --template react
cd ..
# 后端
cd server
npm init -y
npm install express cors dotenv bcryptjs jsonwebtoken sqlite3 axios
npm install -D nodemon
目录结构:
csharp
ai-todo/
├── client/ # React前端
│ ├── src/
│ ├── public/
│ └── package.json
├── server/ # Node后端
│ ├── src/
│ ├── db/
│ └── package.json
└── README.md
Hour 2-4:环境搭建------开发环境即线上环境
关键决策:我不想本地开发完再折腾服务器环境。直接上云开发,开发环境和生产环境保持一致。
我用的是阿里云OPC创业套餐的MVP阶段配置:
- ECS 2核2G(Ubuntu 22.04)
- ESA边缘安全加速(免费版)
- 域名(直接在里面注册)
这里我用阿里云OPC的MVP套餐作为开发+部署一体机。2核2G对于开发阶段完全够用,而且部署时不用再配一遍环境,直接在同一台机器上打包上线。
创建实例后,SSH连接,配置开发环境:
bash
ssh root@你的ECS公网IP
# 安装Node.js 20.x
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt install -y nodejs
# 验证
node -v # v20.x.x
npm -v # 10.x.x
# 安装Git
apt install git -y
# 创建项目目录
mkdir -p /var/www/ai-todo
cd /var/www/ai-todo
# 克隆本地代码(或者用GitHub)
# 这里我直接在服务器上开发,省去本地→服务器的同步步骤
git init
为什么直接在服务器上开发?
- 省去"本地开发→部署到服务器→环境不一致报错"的循环
- 2核2G跑开发服务器+React热更新,完全流畅
- 开发完直接
npm run build,Nginx指向dist目录,秒级上线
Hour 4-12:后端开发------API与数据库
数据库设计(SQLite)
javascript
// server/src/db.js
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const db = new sqlite3.Database(path.join(__dirname, '../db/todo.db'));
// 初始化表
db.serialize(() => {
db.run(`CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
db.run(`CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
title TEXT NOT NULL,
description TEXT,
priority INTEGER DEFAULT 1,
deadline TEXT,
completed INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
)`);
});
module.exports = db;
Express服务器主入口
javascript
// server/src/index.js
const express = require('express');
const cors = require('cors');
const dotenv = require('dotenv');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const db = require('./db');
const axios = require('axios');
dotenv.config();
const app = express();
app.use(cors());
app.use(express.json());
const PORT = process.env.PORT || 3000;
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
// 中间件:验证JWT
const auth = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: '未登录' });
jwt.verify(token, JWT_SECRET, (err, user) => {
if (err) return res.status(403).json({ error: 'Token无效' });
req.user = user;
next();
});
};
// 注册
app.post('/api/register', async (req, res) => {
const { username, password } = req.body;
const hashed = await bcrypt.hash(password, 10);
db.run('INSERT INTO users (username, password) VALUES (?, ?)',
[username, hashed], function(err) {
if (err) return res.status(400).json({ error: '用户名已存在' });
res.json({ id: this.lastID, username });
});
});
// 登录
app.post('/api/login', (req, res) => {
const { username, password } = req.body;
db.get('SELECT * FROM users WHERE username = ?', [username], async (err, user) => {
if (err || !user) return res.status(400).json({ error: '用户不存在' });
const valid = await bcrypt.compare(password, user.password);
if (!valid) return res.status(400).json({ error: '密码错误' });
const token = jwt.sign({ id: user.id, username: user.username }, JWT_SECRET);
res.json({ token, username: user.username });
});
});
// 获取待办列表
app.get('/api/todos', auth, (req, res) => {
db.all('SELECT * FROM todos WHERE user_id = ? ORDER BY priority DESC, deadline ASC',
[req.user.id], (err, rows) => {
if (err) return res.status(500).json({ error: err.message });
res.json(rows);
});
});
// 创建待办
app.post('/api/todos', auth, (req, res) => {
const { title, description, priority, deadline } = req.body;
db.run('INSERT INTO todos (user_id, title, description, priority, deadline) VALUES (?, ?, ?, ?, ?)',
[req.user.id, title, description, priority || 1, deadline],
function(err) {
if (err) return res.status(500).json({ error: err.message });
res.json({ id: this.lastID, title, description, priority, deadline, completed: 0 });
});
});
// 更新待办
app.put('/api/todos/:id', auth, (req, res) => {
const { title, description, priority, deadline, completed } = req.body;
db.run('UPDATE todos SET title=?, description=?, priority=?, deadline=?, completed=? WHERE id=? AND user_id=?',
[title, description, priority, deadline, completed, req.params.id, req.user.id],
function(err) {
if (err) return res.status(500).json({ error: err.message });
res.json({ updated: this.changes });
});
});
// 删除待办
app.delete('/api/todos/:id', auth, (req, res) => {
db.run('DELETE FROM todos WHERE id=? AND user_id=?', [req.params.id, req.user.id],
function(err) {
if (err) return res.status(500).json({ error: err.message });
res.json({ deleted: this.changes });
});
});
// AI:智能优先级排序
app.post('/api/ai/prioritize', auth, async (req, res) => {
const { todos } = req.body; // [{id, title, description, deadline}]
try {
const response = await axios.post('https://api.openai.com/v1/chat/completions', {
model: 'gpt-4o-mini',
messages: [{
role: 'system',
content: '你是一个时间管理助手。请根据任务紧急程度和重要性,为以下待办事项分配优先级(1-5,5最高)。只返回JSON数组,格式:[{"id": 1, "priority": 5}, ...]'
}, {
role: 'user',
content: JSON.stringify(todos)
}]
}, {
headers: { 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}` }
});
const result = JSON.parse(response.data.choices[0].message.content);
res.json(result);
} catch (err) {
res.status(500).json({ error: 'AI服务暂时不可用' });
}
});
// AI:任务拆解
app.post('/api/ai/breakdown', auth, async (req, res) => {
const { title } = req.body;
try {
const response = await axios.post('https://api.openai.com/v1/chat/completions', {
model: 'gpt-4o-mini',
messages: [{
role: 'system',
content: '你是一个项目管理专家。请将用户输入的任务拆解为3-8个可执行的子任务。只返回JSON数组,格式:[{"title": "子任务1", "estimated_hours": 2}, ...]'
}, {
role: 'user',
content: title
}]
}, {
headers: { 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}` }
});
const result = JSON.parse(response.data.choices[0].message.content);
res.json(result);
} catch (err) {
res.status(500).json({ error: 'AI服务暂时不可用' });
}
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
环境变量配置
bash
# server/.env
PORT=3000
JWT_SECRET=your-super-secret-jwt-key-change-this
OPENAI_API_KEY=sk-your-openai-api-key
启动后端:
bash
cd server
npm install
npx nodemon src/index.js
验证:访问 http://你的ECSIP:3000/api/todos,应该返回401(因为没登录),说明API正常运行。
Hour 12-20:前端开发------React + Tailwind
项目初始化
bash
cd client
npm install
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
配置Tailwind:
javascript
// client/tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: { extend: {} },
plugins: [],
}
css
/* client/src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
核心组件:App.jsx
jsx
// client/src/App.jsx
import { useState, useEffect } from 'react';
import Auth from './components/Auth';
import TodoList from './components/TodoList';
import AIBreakdown from './components/AIBreakdown';
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000/api';
function App() {
const [user, setUser] = useState(null);
const [token, setToken] = useState(localStorage.getItem('token'));
useEffect(() => {
if (token) {
// 验证token有效性
fetch(`${API_URL}/todos`, { headers: { Authorization: `Bearer ${token}` } })
.then(res => res.ok ? setUser({ username: 'user' }) : logout())
.catch(() => logout());
}
}, [token]);
const login = (newToken, username) => {
localStorage.setItem('token', newToken);
setToken(newToken);
setUser({ username });
};
const logout = () => {
localStorage.removeItem('token');
setToken(null);
setUser(null);
};
if (!user) return <Auth onLogin={login} apiUrl={API_URL} />;
return (
<div className="min-h-screen bg-gray-50">
<header className="bg-white shadow-sm border-b">
<div className="max-w-4xl mx-auto px-4 py-4 flex justify-between items-center">
<h1 className="text-xl font-bold text-gray-900">🤖 AI待办清单</h1>
<div className="flex items-center gap-4">
<span className="text-sm text-gray-600">{user.username}</span>
<button onClick={logout} className="text-sm text-red-600 hover:text-red-800">
退出
</button>
</div>
</div>
</header>
<main className="max-w-4xl mx-auto px-4 py-8">
<AIBreakdown apiUrl={API_URL} token={token} />
<TodoList apiUrl={API_URL} token={token} />
</main>
</div>
);
}
export default App;
TodoList组件
jsx
// client/src/components/TodoList.jsx
import { useState, useEffect } from 'react';
export default function TodoList({ apiUrl, token }) {
const [todos, setTodos] = useState([]);
const [newTodo, setNewTodo] = useState({ title: '', description: '', deadline: '' });
const [loading, setLoading] = useState(false);
const headers = {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
};
useEffect(() => { fetchTodos(); }, []);
const fetchTodos = async () => {
const res = await fetch(`${apiUrl}/todos`, { headers: { Authorization: `Bearer ${token}` } });
const data = await res.json();
setTodos(data);
};
const addTodo = async (e) => {
e.preventDefault();
if (!newTodo.title) return;
await fetch(`${apiUrl}/todos`, {
method: 'POST',
headers,
body: JSON.stringify(newTodo)
});
setNewTodo({ title: '', description: '', deadline: '' });
fetchTodos();
};
const toggleComplete = async (todo) => {
await fetch(`${apiUrl}/todos/${todo.id}`, {
method: 'PUT',
headers,
body: JSON.stringify({ ...todo, completed: todo.completed ? 0 : 1 })
});
fetchTodos();
};
const deleteTodo = async (id) => {
await fetch(`${apiUrl}/todos/${id}`, { method: 'DELETE', headers: { Authorization: `Bearer ${token}` } });
fetchTodos();
};
const aiPrioritize = async () => {
setLoading(true);
const res = await fetch(`${apiUrl}/ai/prioritize`, {
method: 'POST',
headers,
body: JSON.stringify({ todos: todos.map(t => ({ id: t.id, title: t.title, description: t.description, deadline: t.deadline })) })
});
const priorities = await res.json();
// 批量更新优先级
for (const p of priorities) {
await fetch(`${apiUrl}/todos/${p.id}`, {
method: 'PUT',
headers,
body: JSON.stringify({ ...todos.find(t => t.id === p.id), priority: p.priority })
});
}
fetchTodos();
setLoading(false);
};
const priorityColor = (p) => {
if (p >= 5) return 'bg-red-100 text-red-800 border-red-300';
if (p >= 3) return 'bg-yellow-100 text-yellow-800 border-yellow-300';
return 'bg-green-100 text-green-800 border-green-300';
};
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<h2 className="text-lg font-semibold text-gray-900">我的待办</h2>
<button
onClick={aiPrioritize}
disabled={loading}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 text-sm"
>
{loading ? 'AI分析中...' : '🤖 AI智能排序'}
</button>
</div>
<form onSubmit={addTodo} className="bg-white p-4 rounded-lg shadow-sm border space-y-3">
<input
type="text"
placeholder="任务标题..."
value={newTodo.title}
onChange={e => setNewTodo({...newTodo, title: e.target.value})}
className="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<div className="flex gap-3">
<input
type="text"
placeholder="描述(可选)"
value={newTodo.description}
onChange={e => setNewTodo({...newTodo, description: e.target.value})}
className="flex-1 px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<input
type="date"
value={newTodo.deadline}
onChange={e => setNewTodo({...newTodo, deadline: e.target.value})}
className="px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button type="submit" className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
添加
</button>
</div>
</form>
<div className="space-y-3">
{todos.sort((a, b) => b.priority - a.priority).map(todo => (
<div key={todo.id} className={`bg-white p-4 rounded-lg shadow-sm border flex items-center gap-4 ${todo.completed ? 'opacity-60' : ''}`}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleComplete(todo)}
className="w-5 h-5 text-blue-600"
/>
<div className="flex-1">
<div className="flex items-center gap-2">
<span className={todo.completed ? 'line-through text-gray-500' : 'font-medium text-gray-900'}>
{todo.title}
</span>
<span className={`px-2 py-0.5 text-xs rounded-full border ${priorityColor(todo.priority)}`}>
P{todo.priority}
</span>
</div>
{todo.description && <p className="text-sm text-gray-600 mt-1">{todo.description}</p>}
{todo.deadline && <p className="text-xs text-gray-400 mt-1">截止: {todo.deadline}</p>}
</div>
<button onClick={() => deleteTodo(todo.id)} className="text-red-500 hover:text-red-700 text-sm">
删除
</button>
</div>
))}
</div>
</div>
);
}
AIBreakdown组件(AI任务拆解)
jsx
// client/src/components/AIBreakdown.jsx
import { useState } from 'react';
export default function AIBreakdown({ apiUrl, token }) {
const [task, setTask] = useState('');
const [subtasks, setSubtasks] = useState([]);
const [loading, setLoading] = useState(false);
const breakdown = async () => {
if (!task) return;
setLoading(true);
const res = await fetch(`${apiUrl}/ai/breakdown`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ title: task })
});
const data = await res.json();
setSubtasks(data);
setLoading(false);
};
return (
<div className="bg-gradient-to-r from-blue-50 to-purple-50 p-6 rounded-lg border border-blue-200 mb-8">
<h3 className="text-lg font-semibold text-gray-900 mb-3">🚀 AI任务拆解</h3>
<div className="flex gap-3">
<input
type="text"
placeholder="输入一个复杂任务,比如:准备产品发布会"
value={task}
onChange={e => setTask(e.target.value)}
className="flex-1 px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
onClick={breakdown}
disabled={loading}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
>
{loading ? '拆解中...' : 'AI拆解'}
</button>
</div>
{subtasks.length > 0 && (
<div className="mt-4 bg-white p-4 rounded-lg">
<h4 className="font-medium text-gray-900 mb-2">拆解结果:</h4>
<ul className="space-y-2">
{subtasks.map((st, i) => (
<li key={i} className="flex justify-between items-center text-sm">
<span>• {st.title}</span>
<span className="text-gray-500">预计 {st.estimated_hours} 小时</span>
</li>
))}
</ul>
<p className="text-xs text-gray-400 mt-3">
💡 提示:你可以把这些子任务复制到上方待办列表中逐一完成
</p>
</div>
)}
</div>
);
}
Auth组件(登录/注册)
jsx
// client/src/components/Auth.jsx
import { useState } from 'react';
export default function Auth({ onLogin, apiUrl }) {
const [isLogin, setIsLogin] = useState(true);
const [form, setForm] = useState({ username: '', password: '' });
const [error, setError] = useState('');
const submit = async (e) => {
e.preventDefault();
setError('');
const endpoint = isLogin ? '/login' : '/register';
const res = await fetch(`${apiUrl}${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(form)
});
const data = await res.json();
if (!res.ok) return setError(data.error);
if (isLogin) {
onLogin(data.token, data.username);
} else {
setIsLogin(true);
setError('注册成功,请登录');
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="bg-white p-8 rounded-lg shadow-lg w-full max-w-md">
<h2 className="text-2xl font-bold text-center mb-6">
{isLogin ? '登录' : '注册'}
</h2>
{error && <p className="text-red-600 text-sm mb-4 text-center">{error}</p>}
<form onSubmit={submit} className="space-y-4">
<input
type="text"
placeholder="用户名"
value={form.username}
onChange={e => setForm({...form, username: e.target.value})}
className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
required
/>
<input
type="password"
placeholder="密码"
value={form.password}
onChange={e => setForm({...form, password: e.target.value})}
className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
required
/>
<button type="submit" className="w-full py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
{isLogin ? '登录' : '注册'}
</button>
</form>
<p className="text-center mt-4 text-sm text-gray-600">
{isLogin ? '还没有账号?' : '已有账号?'}
<button
onClick={() => setIsLogin(!isLogin)}
className="text-blue-600 hover:underline ml-1"
>
{isLogin ? '立即注册' : '去登录'}
</button>
</p>
</div>
</div>
);
}
配置前端环境变量:
bash
# client/.env
VITE_API_URL=http://你的ECSIP:3000/api
启动前端开发服务器:
bash
cd client
npm install
npm run dev
此时访问 http://你的ECSIP:5173,应该能看到登录页面。注册一个账号,测试添加待办、AI排序、AI拆解功能。
Hour 20-24:Day 1收尾------联调与Bug修复
这个阶段主要是前后端联调:
- 注册/登录流程正常
- 待办CRUD正常
- AI优先级排序正常(OpenAI API返回JSON格式)
- AI任务拆解正常
- 响应式布局在手机端正常
遇到的坑:
- CORS问题 :开发时前端端口5173,后端3000,需要配置CORS。已在Express中启用
app.use(cors())。 - OpenAI API超时:AI排序如果待办太多,API响应慢。解决方案:限制每次最多传10个待办。
- SQLite并发:SQLite在并发写入时可能锁表。MVP阶段用户量小,暂时忽略。用户量大了迁移到RDS。
Day 2:部署上线(Hour 24-48)
Hour 24-30:生产环境构建
前端构建
bash
cd /var/www/ai-todo/client
# 修改生产环境API地址
# 把 .env 里的 VITE_API_URL 改为你的域名(如果已配好域名)或ECS IP
# 例如:VITE_API_URL=https://你的域名/api
npm run build
# 生成 dist/ 目录
后端生产化
bash
cd /var/www/ai-todo/server
# 安装PM2用于进程管理
npm install -g pm2
# 创建PM2配置
cat > ecosystem.config.js << 'EOF'
module.exports = {
apps: [{
name: 'ai-todo-api',
script: './src/index.js',
instances: 1,
exec_mode: 'fork',
env: {
NODE_ENV: 'production',
PORT: 3000
},
error_file: './logs/err.log',
out_file: './logs/out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z'
}]
};
EOF
mkdir -p logs
pm2 start ecosystem.config.js
pm2 save
pm2 startup
验证后端是否正常运行:
bash
curl http://localhost:3000/api/todos
# 应该返回401(未登录),说明服务正常
Hour 30-36:Nginx配置与域名
Nginx反向代理配置
bash
apt install nginx -y
cat > /etc/nginx/sites-available/ai-todo << 'EOF'
server {
listen 80;
server_name _; # 暂时用IP访问,配好域名后替换
# 前端静态文件
location / {
root /var/www/ai-todo/client/dist;
index index.html;
try_files $uri $uri/ /index.html;
}
# 后端API代理
location /api/ {
proxy_pass http://localhost:3000/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
EOF
ln -s /etc/nginx/sites-available/ai-todo /etc/nginx/sites-enabled/
rm /etc/nginx/sites-enabled/default
nginx -t
systemctl restart nginx
现在访问 http://你的ECSIP,应该能看到前端页面,API也能正常调用。
域名配置(可选但建议做)
在阿里云OPC套餐内注册域名后,添加A记录指向ECS公网IP:
bash
# 修改Nginx配置中的 server_name
sed -i 's/server_name _;/server_name your-domain.com;/' /etc/nginx/sites-available/ai-todo
nginx -t && systemctl reload nginx
Hour 36-42:ESA加速与HTTPS
阿里云OPC套餐包含ESA边缘安全加速免费版,配置非常简单:
- 进入ESA控制台,添加你的域名
- 按提示修改DNS解析为CNAME记录(ESA会提供)
- 开启HTTPS(ESA免费版支持自动SSL证书)
- 开启基础DDoS防护和CDN缓存
这里我用的是阿里云OPC套餐里自带的ESA服务。免费版对于MVP阶段来说,既能加速静态资源,又能自动搞定HTTPS证书,省了我申请Let's Encrypt和配置Certbot的时间。
配置完成后,等待DNS生效(通常5-10分钟),然后访问你的域名,应该能看到HTTPS锁标志。
Hour 42-48:最终验证与上线
验证清单
- 域名可访问,HTTPS正常
- 注册/登录功能正常
- 待办CRUD正常
- AI排序功能正常
- AI拆解功能正常
- 手机端访问正常(响应式布局)
- 页面加载速度 < 2秒(ESA CDN生效)
- 服务器CPU占用 < 50%(2核2G够用)
性能测试
bash
# 服务器端查看资源占用
htop
# 2核2G,运行Node + Nginx + SQLite,CPU占用约15-25%,内存约400MB
上线庆祝
把链接发给朋友,让他们注册试用。收集第一批反馈,准备下一个迭代。
技术总结:这套流水线的核心优势
1. 开发环境即生产环境
直接在ECS上开发,省去"本地开发→部署→环境不一致→调试"的循环。开发完build一下,Nginx指向dist目录,秒级上线。
2. 技术栈极简但够用
- React + Tailwind:前端开发效率极高
- Node + Express + SQLite:后端零配置,单文件数据库,备份就是复制一个文件
- OpenAI API:AI能力无需自研模型,调用即可
3. 部署流程标准化
从代码到上线的流程:
arduino
git pull → npm run build → pm2 reload → nginx reload
全程不超过5分钟。如果配了GitHub Actions,可以自动化到1分钟。
4. 成本控制
- ECS 2核2G:MVP阶段足够支撑1000日活
- SQLite:零成本,零配置
- OpenAI GPT-4o mini:成本极低,1000次调用约$0.15
- ESA免费版:CDN+HTTPS+基础防护,零成本
踩坑记录与解决方案
| 坑 | 解决方案 |
|---|---|
| SQLite并发锁表 | MVP阶段忽略,日活>1000后迁移到RDS |
| OpenAI API偶发超时 | 添加loading状态,超时后提示用户重试 |
| 前端路由刷新404 | Nginx配置 try_files $uri $uri/ /index.html; |
| 环境变量泄露 | 后端.env文件不提交Git,服务器手动配置 |
| 静态资源缓存 | ESA CDN自动处理,无需手动配置 |
下一步优化方向
- 数据库迁移:用户量上来后,SQLite→RDS MySQL,阿里云OPC增长阶段套餐包含RDS
- AI模型降级:GPT-4o mini→更便宜的模型,或接入阿里云百炼平台
- 添加Redis:缓存AI响应结果,减少API调用成本
- CI/CD:GitHub Actions自动部署到ECS
写在最后
48小时上线一个完整产品,听起来很夸张,但实际上:当技术选型足够极简、开发环境足够统一、部署流程足够标准化时,这是完全可行的。
关键不是写了多少代码,而是省去了多少决策时间和配置时间。阿里云OPC的MVP套餐帮我把"选型+配置"的时间从几天压缩到了30分钟,让我能把精力集中在产品功能上。
如果你也想尝试48小时挑战,我的建议是:
- 选一个你熟悉的技术栈(不要学新技术)
- 选一个按阶段打包的云套餐(不要自己配服务器)
- 功能砍到不能再砍(只保留核心流程)
- 先上线,再迭代
Done is better than perfect.
参考资料与工具清单
- React 18官方文档
- Tailwind CSS文档
- Express.js指南
- OpenAI API文档
- PM2进程管理文档