利用 Vely 在 Linux 构建你自己的 SaaS
Vely 可让你在网络应用程序中利用 C 语言的强大功能。
Vely SaaS
Vely 可让你在网络应用程序中利用 C 语言的强大功能。
![](/data/attachment/album/202312/12/110902myyfcm3hdmwqv3zy.jpg)



> 
> Vely 可让你在网络应用程序中利用 C 语言的强大功能。
> 
> 
> 


[Vely](https://opensource.com/article/22/5/write-c-appplications-vely-linux) 将 C 语言的高性能和低内存占用与 PHP 等语言的易用性和安全性相结合。作为自由开源软件，它以 GPLv3 和 LGPL 3 授权，所以你甚至可以用它来构建商业软件。


### 利用 Vely 构建 SaaS


你可以使用 Vely 创建一个多租户网络应用程序，它可以作为软件即服务模式（SaaS）在互联网上运行。每个用户都有一个完全独立的数据空间。


在这个网络应用程序示例中，用户可以注册一个笔记本服务来创建笔记，然后查看和删除它们。它仅用了 7 个源文件，310 行代码，就展示了如何集成多项技术:


* MariaDB
* 网络浏览器
* Apache
* Unix 套接字


#### 运作原理


以下是从用户的角度来看应用程序是如何工作的。下图是代码演示。


该应用允许用户通过指定电子邮件地址和密码创建新的登录名。你可以用任何你喜欢的方式设置它们，例如运用 CSS：


![创建一个用户账户](/data/attachment/album/202312/12/110946trmkfsf1l1m6ylr1.png)


验证用户的电子邮件:


![验证用户的电子邮件地址](/data/attachment/album/202312/12/110946vow5z3f1qpsvxqpv.png)


每个用户使用自己独有的用户名和密码登录：


![用户登录](/data/attachment/album/202312/12/110946qsntseszsji9xies.png)


一旦登录，用户就可以添加笔记：


![用户可以添加笔记](/data/attachment/album/202312/12/110947be001fcv110een44.png)


用户可以获取笔记列表：


![用户列举笔记](/data/attachment/album/202312/12/110947ffc1f6vcbefpcfp8.png)


删除笔记之前，应用会申请确认信息：


![删除笔记之前，应用会申请确认信息](/data/attachment/album/202312/12/110947o70sv2jxqb0sk0kj.png)


用户确认后，笔记被删除：


![用户确认后，笔记被删除](/data/attachment/album/202312/12/110948p13ha0s2281zm44u.png)


#### 设置先决条件


遵照 [Vely.dev](https://vely.dev/) 上的安装指示。这是一个使用 DNF、APT、Pacman 或者 Zypper 等标准工具包的快速流程。


由于它们都是这个范例的一部分，你必须安装 Apache 作为网络服务器，安装 MariaDB 作为数据库。


安装 Vely 后，如使用 Vim，打开里面的“语法高亮显示”：



```
vv -m

```

#### 获取源代码


这个演示 SaaS 应用程序的源代码是 Vely 安装的一部分。为每个应用程序创建一个单独的源代码目录不失为一个好主意（而且你可以按自己喜好命名）。在这种情况下，解包源代码会帮你完成这些工作:



```
$ tar xvf $(vv -o)/examples/multitenant_SaaS.tar.gz
$ cd multitenant_SaaS

```

默认情况下，该应用程序以 `multitenant_SaaS` 命名，但你可以将其命名为任何内容（如果这么做，其他每个地方你都需要改一下）。


### 创建应用程序


第一步是创建一个应用程序。使用 Vely 的 `vf` 工具就可以轻松完成：



```
$ sudo vf -i-u $(whoami) multitenant_SaaS

```

这个命令创建了一个新的应用程序主目录（`/var/lib/vv/multitenant_SaaS`），并帮你执行应用程序设置。通常，这意味着在该主目录中创建各种子目录并分配权限。在这种情况下，只有当前用户（`whoami` 的结果）拥有目录，具有 `0700` 权限，这确保了其他人没有访问文件的权限。


### 创建数据库


在你键入任何代码之前，你需要一个能够存储该应用程序所用信息的空间。首先，创建一个名为 `db_multitenant_SaaS` 的 MariaDB 数据库，由用户名为 `vely` 的用户所有，密码为 `your_password` 。你可以修改刚才提到的任何值，但得记住，在这个示例里，你需要将包含这些内容的每个地方都得修改一遍。


在 MySQL 中以 root 身份登录：



```
create database if not exists db_multitenant_SaaS;
create user if not exists vely identified by 'your_password';
grant create,alter,drop,select,insert,delete,update on db_multitenant_SaaS.* to vely;

```

然后在数据库内创建数据库对象（表，记录等等）：



```
use db_multitenant_SaaS;
source setup.sql;
exit

```

### 将 Vely 连接至数据库


为了让 Vely 知晓你数据库的位置以及如何登录进去，创建一个名为 `db_multitenant_SaaS` 的数据库配置文件。（该名称用于在源代码中的数据库声明，所以如果你改了它，确保在它存在的每个地方都改一遍。）


Vely 使用原生的 MariaDB 数据库连接，因此你可以指定给定的数据库所能允许的任何选项:



```
$ echo '[client]
user=vely
password=your_password
database=db_multitenant_SaaS
protocol=TCP
host=127.0.0.1
port=3306' > db_multitenant_SaaS

```

### 构建应用程序


使用 `vv` 工具构建应用程序，利用 `--db` 选项指定 MariaDB 数据库和数据库配置文件：



```
$ vv -q--db=mariadb:db_multitenant_SaaS

```

### 启用应用服务器


启动你的网络应用程序的服务器，需要使用 `vf` FastCGI 进程管理器。应用程序服务器使用 Unix 套接字与网络服务器（创建反向代理）通信：



```
$ vf -w3 multitenant_SaaS

```

这么做会启用三个守护进程，为接收到的请求提供服务。你也可以启动一个自适应服务器，它会增加进程的数量从而服务更多的请求，并在不需要他们时减少进程的数量：



```
$ vf multitenant_SaaS

```

请参阅 `vf` 了解更多选项，以帮助你实现最佳性能。


当你需要停止你的应用程序服务器，使用 `-m quit` 选项：



```
$ vf -m quit multitenant_SaaS

```

### 创建网络服务器


这是一个网络应用程序，那么应用程序就得需要一个网络服务器。该示例通过一个 Unix 套接字监听器使用 Apache。


#### 1、设置 Apache


将 Apache 配置为一个反向代理，并将你的应用程序与之连接，你需要启用 FastCGI 代理支持，这通常使用 `proxy` 和 `proxy_fcgi` 模块。


对于 Fedora 系统（或者其它的，比如 Arch）来说，通过在 Apache 配置文件 `/etc/httpd/conf/httpd.conf` 中添加（或取消注释）适当的 `LoadModule` 指令，就可启用 `proxy` 和 `proxy_fcgi` 模块。


以下指令适用于 Debian，Ubuntu 以及类似的系统，启用 `proxy` 和 `proxy_fcgi` 模块：



```
$ sudo a2enmod proxy
$ sudo a2enmod proxy_fcgi

```

以下指令适用于 OpenSUSE，将这几行添加在 `/etc/apache2/httpd.conf` 结尾处：



```
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so

```

#### 2、配置 Apache


现在你必须将代理信息添加在 Apache 的配置文件中：



```
ProxyPass "/multitenant_SaaS" unix:///var/lib/vv/multitenant_SaaS/sock/sock|fcgi://localhost/multitenant_SaaS

```

你的配置文件的位置可能会有所不同，这取决于不同的 Linux 发行版：


* Fedora、CentOS、Mageia 和 Arch： `/etc/httpd/conf/httpd.conf`
* Debian、Ubuntu、Mint： `/etc/apache2/apache2.conf`
* OpenSUSE：`/etc/apache2/httpd.conf`


#### 3、重新启动


最后，重启 Apache。在 Fedora 和类似系统，还有 Arch Linux 是如下指令：



```
$ sudo systemctl restart httpd

```

在 Debian 和基于 Debian 的系统，还有 OpenSUSE 是如下指令：



```
$ sudo systemctl restart apache2

```

### 设置本地邮箱


这个示例中，电子邮件是其功能的一部分。如果你的服务器已经可以发送电子邮件了，你可以跳过这一条。此外，你可以使用本地邮箱（`myuser@localhost`）来测试它。要做到这一点，需安装 Sendmail。


在 Fedora 和类似系统中是如下指令：



```
$ sudo dnf installsendmail
$ sudo systemctl start sendmail

```

而在 Debian 和类似系统（如 Ubuntu）：



```
$ sudo apt installsendmail
$ sudo systemctl start sendmail

```

当应用程序向本地用户发送电子邮件，比如说 `OS_user@localhost`，你就可以通过查看 `/var/mail/` 处（即所谓“邮件池”）来确认电子邮件是否被发送。


### 从浏览器访问应用服务器


假设你在本地运行该应用，可以通过使用 `http://127.0.0.1/multitenant_SaaS?req=notes&action=begin` 域名从你的网络服务器访问你的应用服务器。如果你在互联网上的在线服务器运行该程序，你可能就需要调整防火墙设置以允许 HTTP 通信。


### 源代码


该应用程序示例包含 7 个源文件。你可以自行回顾代码（记住，这些文件只有 310 行代码），下面是每个文件的概述。


#### SQL 设置（setup.sql）


创建的两个表：


* `users`：每个用户的信息。在 `users` 表中，每个用户都有自己唯一的 ID （`userId` 列），以及其他信息，如电子邮件地址和该地址是否通过了验证。还有一个哈希密码。实际的密码永远不会存储在纯文本（或其他形式）中，单向哈希用于检查密码。
* `notes`：用户输入的笔记。`notes` 表包含了所有的笔记，每个笔记都有一个 `userId` 列，表示哪个用户拥有它们。`userId` 列的值与 `users` 表中的同名列匹配。这样，每个笔记显然都属于单个用户。


该文件内容如下：



```
create table if not exists notes (dateOf datetime, noteId bigint auto_increment primary key, userId bigint, note varchar(1000));
create table if not exists users (userId bigint auto_increment primary key, email varchar(100), hashed_pwd varchar(100), verified smallint, verify_token varchar(30), session varchar(100));
create unique index if not exists users1 on users (email);

```

#### 运行时数据（login.h）


为了正确地显示登录、注册和注销链接，你需要一些在应用程序中任何地方都可以使用的标志。此外，应用程序使用 cookie 来维护会话，因此它需要在任何地方都可用，例如，验证会话是否有效。发送到应用程序的每个请求都以这种方式进行确认。只有带有可验证 cookie 的请求是允许的。


所以要做到这种效果，你需要有一个 `global_request_data` 类型的 `reqdata`（请求数据），其中包含 `sess_userId`（用户的 ID）以及 `sess_id`（用户目前的会话 ID）。此外，还有一些不言自明的标志，可以帮助渲染页面：



```
#ifndef _VV_LOGIN
#define _VV_LOGIN

typedef struct s_reqdata {
    bool displayed_logout; // true 则显示登出连接
    bool is_logged_in; // true 则会话已验证登录
    char *sess_userId; // 目前会话的用户 ID
    char *sess_id; // 会话 ID
} reqdata;

void login_or_signup ();

#endif

```

#### 会话检查和会话数据（\_before.vely）


Vely 里有一个 <ruby> 请求前处理程序 <rt>  before_request handler </rt></ruby> 的概念。你写的代码会在其它处理请求的代码之前执行的。要达到这个目的，你只需要将这样的代码写在名为 `_before.vely` 的文件中，然后剩余的部分将会自动处理。


SaaS 应用程序所作的任何事情，例如处理发送至应用程序的请求，必须验证其安全性。这样，应用程序就能知晓调用方是否有执行操作所需要的权限。


在这里，通过请求前处理程序进行权限检查。这样，无论其他代码如何处理请求，都已经掌握了会话信息。


为保持会话数据（比如会话 ID 和用户 ID）在你代码中的任何地方都可用，你可以使用 `global_request_data`。它只是一个指向内存的通用指针（`void*`），任何处理请求的代码都可以访问它。这非常适合处理会话，如下所示:



```
#include "vely.h"
#include "login.h"

// _before() 是一个请求前处理程序。
// 它总是在处理请求的其他代码之前执行。
// 对于任何类型的请求范围设置或数据初始化，它都是一个很好的位置。
void _before() {
    // 输出 HTTP 请求头
    out-header default
    reqdata *rd; // 这是全局请求数据，见 login.h
    // 为全局请求数据分配内存，
    // 将在请求结束时自动释放
    new-mem rd size sizeof(reqdata)
    // 初始化标志
    rd->displayed_logout = false;
    rd->is_logged_in = false;
    // 将我们创建的数据设置为全局请求数据，
    // 可以从任何处理请求的代码中访问
    set-req data rd
    // 检查会话是否存在（基于来自客户端的 cookie）
    // 这在任何其他请求处理代码之前执行，
    // 使其更容易准备好会话信息
    _check_session ();
}

```

#### 检查会话是否有效（\_check\_session.vely）


多租户 SaaS 应用程序中最重要的任务之一就是通过检查用户是否登录来（尽快）检查会话是否有效。这是通过从客户端（例如网络浏览器）获取会话 ID 和用户 ID 的 cookie，并将它们与存储会话的数据库进行比较来实现的：



```
#include "vely.h"
#include "login.h"


// 检查会话是否有效
void _check_session () {
    // 获取全局请求数据
    reqdata *rd;
    get-req data to rd
    // 自用户浏览器获取 cookies
    get-cookie rd->sess_userId="sess_userId"
    get-cookie rd->sess_id="sess_id"
    if (rd->sess_id[0] != 0) {
        // 检查给定用户 ID 下的会话 ID 是否正确
        char *email;
        run-query @db_multitenant_SaaS = "select email from users where userId='%s' and session='%s'" output email : rd->sess_userId, rd->sess_id row-count define rcount
            query-result email to email
        end-query
        if (rcount == 1) {
            // 如果正确，设置登录标志
            rd->is_logged_in = true;
            // 如果登出链接不显示，则显示它
            if (rd->displayed_logout == false) {
                @Hi <<p-out email>>! <a href="https://opensource.com/?req=login&action=logout">Logout</a><br/>
                rd->displayed_logout = true;
            }
        } else rd->is_logged_in = false;
    }
}

```

#### 注册、登录、登出（login.vely）


任何多租户系统的基础便是具有用户注册\登录和登出的功能。通常情况下，注册包括验证电子邮件地址；不止于此，同一电子邮件地址会作为一个用户名。这里就是这种情况。


这里实现了几个执行该功能所必须的子请求：


* 注册新用户时，显示 HTML 表单以收集信息。它的 URL 请求签名是 `req=login&action=newuser`。
* 作为对注册表单的响应，创建一个新用户。URL 请求的签名是 `req=login&action=createuser`。输入参数（`input-param`）信号获取 `email` 和 `pwd` 的 POST 表单字段。密码值是单向散列，电子邮件验证令牌是一个随机的 5 位数字。这些被插入到 `users` 表中，创建一个新用户。系统会发送一封验证邮件，并提示用户阅读邮件并输入代码。
* 通过输入发送到该电子邮件的验证码来验证电子邮件。URL 请求的签名是 `req=login&action=verify`。
* 显示一个登录表单，让用户登录。URL 请求的签名是 `req=login`（例如，`action` 为空）。
* 通过验证电子邮件地址（用户名）和密码登录。URL 请求的签名是 `req=login&action=login`。
* 应用户要求登出。URL 请求的签名是 `req=login&action=logout`。
* 应用程序的登录页。URL 请求的签名是 `req=login&action=begin`。
* 如果用户当前已登录，转到应用程序的登录页面。


可以看看下面这些例子：



```
#include "vely.h"
#include "login.h"

// 处理云端多租户应用程序的会话维护、登录、注销、会话验证
void login () {
    // 获取 URL 的输入参数 `action`
    input-param action

    // 获取全局请求数据，我们在其中记录会话信息，所以它很方便
    reqdata *rd;
    get-req data to rd

    // 如果会话已经建立，我们不会
    // 继续到应用程序主页的唯一原因是我们正在登出
    if (rd->is_logged_in) {
        if (strcmp(action, "logout")) {
            _show_home();
            exit-request
        }
    }

    // 应用程序页面启动。显示登录或注册的链接，
    // 并显示适当的主屏幕
    if (!strcmp (action, "begin")) {
        _show_home();
        exit-request

    // 开始创建新用户。询问电子邮件和密码，
    // 然后提交此表单时创建用户。
    } else if (!strcmp (action, "newuser")) {
        @Create New User<hr/>
        @<form action="https://opensource.com/?req=login" method="POST">
        @<input name="action" type="hidden" value="createuser">
        @<input name="email" type="text" value="" size="50" maxlength="50" required autofocus placeholder="Email">
        @<input name="pwd" type="password" value="" size="50" maxlength="50" required placeholder="Password">
        @<input type="submit" value="Sign Up">
        @</form>

    // 验证用户发送到电子邮件的代码。代码必须匹配，从而验证电子邮件地址   
    } else if (!strcmp (action, "verify")) {
        input-param code
        input-param email
        // 获取基于电子邮件的验证令牌
        run-query @db_multitenant_SaaS = "select verify_token from users where email='%s'" output db_verify : email
            query-result db_verify to define db_verify
            // 将数据库中记录的令牌与用户提供的令牌进行比较
            if (!strcmp (code, db_verify)) {
                @Your email has been verifed. Please <a href="https://opensource.com/?req=login">Login</a>.
                // 如果匹配，更新用户信息以表明已验证。
                run-query @db_multitenant_SaaS no-loop = "update users set verified=1 where email='%s'" : email
                exit-request
            }
        end-query
        @Could not verify the code. Please try <a href="https://opensource.com/?req=login">again</a>.
        exit-request

    // 创建用户 —— 当用户使用电子邮件和密码提交表单以创建用户时运行    
    } else if (!strcmp (action, "createuser")) {
        input-param email
        input-param pwd
        // 创建散列（单向）密码
        hash-string pwd to define hashed_pwd
        // 生成随机的 5 位数字字符串验证代码
        random-string to define verify length 5 number
        // 创建用户：插入电子邮件、哈希密码、验证令牌。当前验证状态为 0，或未验证
        begin-transaction @db_multitenant_SaaS
        run-query @db_multitenant_SaaS no-loop = "insert into users (email, hashed_pwd, verified, verify_token, session) values ('%s', '%s', '0', '%s', '')" : email, hashed_pwd, verify affected-rows define arows error define err on-error-continue
        if (strcmp (err, "0") || arows != 1) {
            // 如果不能添加用户，则可能该用户不存在。不管怎样，我们都无法继续。
            login_or_signup();
            @User with this email already exists.
            rollback-transaction @db_multitenant_SaaS
        } else {
            // 创建带有验证码的电子邮件并将其发送给用户
            write-string define msg
                @From: vely@vely.dev
                @To: <<p-out email>>
                @Subject: verify your account
                @
                @Your verification code is: <<p-out verify>>
            end-write-string
            exec-program "/usr/sbin/sendmail" args "-i", "-t" input msg status define st
            if (st != 0) {
                @Could not send email to <<p-out email>>, code is <<p-out verify>>
                rollback-transaction @db_multitenant_SaaS
                exit-request
            }
            commit-transaction @db_multitenant_SaaS
            // 通知用户查看邮件并输入验证码
            @Please check your email and enter verification code here:
            @<form action="https://opensource.com/?req=login" method="POST">
            @<input name="action" type="hidden" value="verify" size="50" maxlength="50">
            @<input name="email" type="hidden" value="<<p-out email>>">
            @<input name="code" type="text" value="" size="50" maxlength="50" required autofocus placeholder="Verification code">
            @<button type="submit">Verify</button>
            @</form>
        }

    // 这里在登录用户登出时运行    
    } else if (!strcmp (action, "logout")) {
        // 更新用户表以清除会话，即没有该用户登录
        if (rd->is_logged_in) {
            run-query @db_multitenant_SaaS = "update users set session='' where userId='%s'" : rd->sess_userId no-loop affected-rows define arows
            if (arows == 1) {
                rd->is_logged_in = false; // 提示用户未登录
                @You have been logged out.<hr/>
            }
        }
        _show_home();

    // 登录：当用户输入用户名和密码时运行
    } else if (!strcmp (action, "login")) {
        input-param pwd
        input-param email
        // 创建单向散列，目的是与用户表进行比较 —— 密码**永远不会**被记录
        hash-string pwd to define hashed_pwd
        // 为会话 ID 创建一个随机的 30 位长的字符串
        random-string to rd->sess_id length 30
        // 检查用户名和哈希密码是否匹配
        run-query @db_multitenant_SaaS = "select userId from users where email='%s' and hashed_pwd='%s'" output sess_userId : email, hashed_pwd
            query-result sess_userId to rd->sess_userId
            // 如果匹配，使用会话 ID 更新用户表
            run-query @db_multitenant_SaaS no-loop = "update users set session='%s' where userId='%s'" : rd->sess_id, rd->sess_userId affected-rows define arows
            if (arows != 1) {
                @Could not create a session. Please try again. <<.login_or_signup();>> <hr/>
                exit-request
            }
            // 设置“用户 ID”和“会话 ID”为 cookie。用户的浏览器将在每个请求中返回这些信息
            set-cookie "sess_userId" = rd->sess_userId
            set-cookie "sess_id" = rd->sess_id
            // 显示主页，确保会话是正确的，并设置标志
            _check_session();
            _show_home();
            exit-request
        end-query
        @Email or password are not correct. <<.login_or_signup();>><hr/>

    // 登录界面，要求用户输入用户名和密码  
    } else if (!strcmp (action, "")) {
        login_or_signup();
        @Please Login:<hr/>
        @<form action="https://opensource.com/?req=login" method="POST">
        @<input name="action" type="hidden" value="login" size="50" maxlength="50">
        @<input name="email" type="text" value="" size="50" maxlength="50" required autofocus placeholder="Email">
        @<input name="pwd" type="password" value="" size="50" maxlength="50" required placeholder="Password">
        @<button type="submit">Go</button>
        @</form>
    }
}

// 显示登录或注册链接
void login_or_signup() {
        @<a href="https://opensource.com/?req=login">Login</a> & & <a href="https://opensource.com/?req=login&action=newuser">Sign Up</a><hr/>
}

```

#### 通用应用程序（\_show\_home.vely）


借助本教程，你可以创建你想要的任何多租户 SaaS 应用程序。上面的多租户处理模块（`login.vely`）调用 `_show_home()` 函数，它可以容纳你的任何代码。这个示例代码展示了笔记应用程序，但它可以是任何内容。`_show_home()` 函数可以调用你想要的任何代码，它是一个通用的多租户应用程序插件：



```
#include "vely.h"

void _show_home() {
    notes();
    exit-request
}

```

#### 笔记应用程序（notes.vely）


该应用程序能够添加、列举以及删除任何给定的笔记：



```
#include "vely.h"
#include "login.h"

// 多租户云中的笔记应用程序
void notes () {
    // 获取全局请求数据
    reqdata *rd;
    get-req data to rd
    // 如果会话有效，显示登录或注册
    if (!rd->is_logged_in) {
        login_or_signup();
    }
    // 问候用户
    @<h1>Welcome to Notes!</h1><hr/>
    // 如果没有登出，退出 —— 这里确保对用户身份的安全验证
    if (!rd->is_logged_in) {
        exit-request
    }
    // 获取 URL 参数，告诉笔记要做什么
    input-param subreq
    // 显示笔记能够做什么操作（添加或列举笔记）
    @<a href="https://opensource.com/?req=notes&subreq=add">Add Note</a> <a href="https://opensource.com/?req=notes&subreq=list">List Notes</a><hr/>

    // 列举该用户的所有笔记
    if (!strcmp (subreq, "list")) {
        // **只**选取该用户的笔记
        run-query @db_multitenant_SaaS = "select dateOf, note, noteId from notes where userId='%s' order by dateOf desc" : rd->sess_userId output dateOf, note, noteId
            query-result dateOf to define dateOf
            query-result note to define note
            query-result noteId to define noteId
            // 使用快速缓存正则表达式将新行更改为<br/>
            match-regex "\n" in note replace-with "<br/>\n" result define with_breaks status define st cache
            if (st == 0) with_breaks = note; // 什么都没有发现/替换，只用原来的
            // 显示笔记
            @Date: <<p-out dateOf>> (<a href="https://opensource.com/?req=notes&subreq=delete_note_ask&note_id=%3C%3Cp-out%20noteId%3E%3E">delete note</a>)<br/>
            @Note: <<p-out with_breaks>><br/>
            @<hr/>
        end-query
    }

    // 要求删除笔记
    else if (!strcmp (subreq, "delete_note_ask")) {
        input-param note_id
        @Are you sure you want to delete a note? Use Back button to go back, or <a href="https://opensource.com/?req=notes&subreq=delete_note&note_id=%3C%3Cp-out%20note_id%3E%3E">delete note now</a>.
    }

    // 删除笔记
    else if (!strcmp (subreq, "delete_note")) {
        input-param note_id
        // 删除笔记
        run-query @db_multitenant_SaaS = "delete from notes where noteId='%s' and userId='%s'" : note_id, rd->sess_userId affected-rows define arows no-loop error define errnote
        // 告知用户状态
        if (arows == 1) {
            @Note deleted
        } else {
            @Could not delete note (<<p-out errnote>>)
        }
    }

    // 添加笔记
    else if (!strcmp (subreq, "add_note")) {
        // 从 note 表单中获取 URL POST 数据
        input-param note
        // 在该用户的 ID 下插入笔记
        run-query @db_multitenant_SaaS = "insert into notes (dateOf, userId, note) values (now(), '%s', '%s')" : rd->sess_userId, note affected-rows define arows no-loop error define errnote
        // 告知用户状态
        if (arows == 1) {
            @Note added
        } else {
            @Could not add note (<<p-out errnote>>)
        }
    }

    // 显示一个 HTML 表单来收集笔记，并将其发送回这里（使用 subreq="add_note" URL 参数）
    else if (!strcmp (subreq, "add")) {
        @Add New Note
        @<form action="https://opensource.com/?req=notes" method="POST">
        @<input name="subreq" type="hidden" value="add_note">
        @<textarea name="note" rows="5" cols="50" required autofocus placeholder="Enter Note"></textarea>
        @<button type="submit">Create</button>
        @</form>
    }
}

```

### 具有 C 性能的 SaaS


Vely 语言使得 C 语言在你的网络应用程序中得到充分利用这件事成为可能。多租户 SaaS 应用程序便是从中受益的一个典型用例。


看一看参考代码示例，写一写代码，然后试试 Vely。


*（题图：DA/126624c8-1a47-481b-b149-92273e8e0f4f）*




---


via: <https://opensource.com/article/22/11/build-your-own-saas-vely>


作者：[Sergio Mijatovic](https://opensource.com/users/vely) 选题：[lkxed](https://github.com/lkxed) 译者：[Drwhooooo](https://github.com/Drwhooooo) 校对：[wxy](https://github.com/wxy)


本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出

