独立开发者的AI编程流水线:从需求到上线的48小时实录

标签:独立开发 | 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任务拆解正常
  • 响应式布局在手机端正常

遇到的坑

  1. CORS问题 :开发时前端端口5173,后端3000,需要配置CORS。已在Express中启用 app.use(cors())
  2. OpenAI API超时:AI排序如果待办太多,API响应慢。解决方案:限制每次最多传10个待办。
  3. 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边缘安全加速免费版,配置非常简单:

  1. 进入ESA控制台,添加你的域名
  2. 按提示修改DNS解析为CNAME记录(ESA会提供)
  3. 开启HTTPS(ESA免费版支持自动SSL证书)
  4. 开启基础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自动处理,无需手动配置

下一步优化方向

  1. 数据库迁移:用户量上来后,SQLite→RDS MySQL,阿里云OPC增长阶段套餐包含RDS
  2. AI模型降级:GPT-4o mini→更便宜的模型,或接入阿里云百炼平台
  3. 添加Redis:缓存AI响应结果,减少API调用成本
  4. CI/CD:GitHub Actions自动部署到ECS

写在最后

48小时上线一个完整产品,听起来很夸张,但实际上:当技术选型足够极简、开发环境足够统一、部署流程足够标准化时,这是完全可行的

关键不是写了多少代码,而是省去了多少决策时间和配置时间。阿里云OPC的MVP套餐帮我把"选型+配置"的时间从几天压缩到了30分钟,让我能把精力集中在产品功能上。

如果你也想尝试48小时挑战,我的建议是:

  1. 选一个你熟悉的技术栈(不要学新技术)
  2. 选一个按阶段打包的云套餐(不要自己配服务器)
  3. 功能砍到不能再砍(只保留核心流程)
  4. 先上线,再迭代

Done is better than perfect.


参考资料与工具清单

  • React 18官方文档
  • Tailwind CSS文档
  • Express.js指南
  • OpenAI API文档
  • PM2进程管理文档
相关推荐
SkyWalking中文站1 天前
认识 Horizon UI · 11/17:运行时规则与实时调试
运维·监控·自动化运维
SkyWalking中文站3 天前
认识 Horizon UI · 6/17:Trace 探索器
运维·监控·自动化运维
SkyWalking中文站4 天前
认识 Horizon UI · 5/17:3D 基础设施地图
运维·监控·自动化运维
说了很好6 天前
基于有限状态机的模块化 PLC 多色物料分拣容错控制系统设计
自动化运维
说了很好7 天前
工业通用 PLC 分拣模板!传感器去抖 + 气缸互锁 + 状态机 + 超时报警全套
自动化运维
SelectDB13 天前
Litefuse 开源并推出单进程轻量模式,25 秒就能跑起来的 Agent 可观测与评估平台
运维·后端·自动化运维
小林ixn13 天前
别再手写Prompt了!用AI Loop实现自动化自我迭代,效率提升10倍
人工智能·自动化运维
用户5569188175313 天前
#从脚本到独立程序:Python + Playwright 批量抓取的完整踩坑记录
python·自动化运维