# C++系统的结构

首先认识下整个C++服务系统的结构,以用户 kmyckj 的系统 prj_c 为例,如:

├─ kedao
│  ├─ .vscode                                    // VS Code 远程连接到服务器时,打开的工作路径:/opt/kedao,即kedao中间件的安装路径
│  ├─ bin                                        // 可执行程序的目录,服务发布后也在此目录的 lib/用户帐号/系统名称 下
│  ├─ document                                   // 文档存放目录
│  ├─ projects                                   // 前端工程目录
│  ├─ publish                                    // 发布文件目录
│  ├─ src_c                                      // C++服务工程源代码目录
│  │  ├─ include                                 // 引用文件
│  │  │  ├─ AIGCJson.hpp                         // Json字符串与对象转换类
│  │  │  ├─ Cinatra_httpClient.h                 // HttpClient 类
│  │  │  ├─ kedao_public.h                       // kedao 一些常用函数
│  │  │  ├─ openssl_crypto.h                     // 常用的 OpenSSL 加解密函数
│  │  │  └─ ...                                  // 其他文件
│  │  └─ kmyckj                                  // 主用户帐号名称,开发服务器归属用户的用户名称,只有一个
│  │     ├─ prj_c                                // 系统工程源码目录,名称随着系统名称而变化,可以创建任意套系统
│  │     │  ├─ debugger                          // 调试程序目录,不同的开发者,有着不同的调试程序
│  │     │  ├─ model                             // 数据结构类文件目录
│  │     │  │  └─ tb_struct.hpp                  // 数据结构文件
│  │     │  ├─ modules                           // 模块目录
│  │     │  │  ├─ atom_service                   // 原子服务模块
│  │     │  │  ├─ basic_func_employee            // 默认的员工模块
│  │     │  │  ├─ basic_func_funcRounter         // 默认的功能路由模块
│  │     │  │  ├─ basic_func_organization        // 默认的组织机构模块
│  │     │  │  ├─ basic_func_post                // 默认的岗位模块
│  │     │  │  ├─ basic_func_role_permission     // 默认的角色权限模块
│  │     │  │  ├─ mdl_test                       // 如:在讲解注册服务时增加的示例模块
│  │     │  │  │  ├─ public                      // 模块内的公共文件目录
│  │     │  │  │  │  ├─ f_custom_dll_init.hpp    // 每个模块下,都有一个自定义的初始化公共函数文件
│  │     │  │  │  │  └─ ...                      // 自己增加的公共源码文件
│  │     │  │  │  ├─ service                     // 服务文件目录
│  │     │  │  │  │  ├─ svc_test_query_function.hpp // 服务源码文件,与服务名称同名的 *.hpp 文件,如:在讲解注册服务时增加的示例服务
│  │     │  │  │  │  └─ ...                      // 其他服务的源码文件
│  │     │  │  │  ├─ makefile                    // 模块动态库的 makefile 文件
│  │     │  │  │  └─ mdl_test.cpp                // 模块动态库的 main 文件,与模块同名的 *.cpp 文件
│  │     │  │  ├─ sys_commons                    // 默认的公共服务模块
│  │     │  │  ├─ sys_login                      // 默认的登录模块
│  │     │  │  └─ ...                            // 其他模块
│  │     │  ├─ public                            // 公共函数目录
│  │     │  ├─ utils                             // 工具类目录
│  │     │  │  ├─ f_check_user_login.hpp         // 调用服务的身份认证函数文件
│  │     │  │  ├─ f_global_variable.hpp          // 全局变量文件,在整个系统工程有效
│  │     │  │  ├─ f_public.hpp                   // 一些公共函数
│  │     │  │  ├─ init_dll_global.hpp            // 全局的动态库初始化函数
│  │     │  │  ├─ OtlV4Helper.hpp                // 数据库帮助类
│  │     │  │  ├─ sqlite3_helper.hpp             // SQLite数据库帮助类
│  │     │  │  └─ ...                            // 其他工具类文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

这是默认的系统结构,如果自己有更优秀的系统框架,可以替换成自己的框架。

注意:如果替换成自己的框架,需要保证 系统名称、动态库名称和服务名称之间的关系,以及服务的参数正确。

kedao中间件调用服务的关系:

1、文件目录关系:bin/lib/用户名/系统名/lib模块名.so;

2、在 lib模块名.so 中存在与服务同名且参数相同的函数。

# 重点掌握 utils 下的代码

在进行服务开发之前,一定要熟悉 utils 下的这几个源码文件:

# 用户登录身份验证的逻辑,当然,也可以更改成自己的实现逻辑
f_check_user_login.hpp

# 要掌握一些全局变量的具体含义及如何使用
f_global_variable.hpp

# 数据访问帮助类,重点掌握,甚至还要了解下 otlv4.h 的用法,功能已满足绝大部分应用的需求,也可以自己扩展功能
OtlV4Helper.hpp
1
2
3
4
5
6
7
8

# 模块的文件结构

modules                             // 模块目录
  ├─ mdl_test                       // 模块名称
  │  ├─ public                      // 模块内的公共文件目录
  │  │  ├─ f_custom_dll_init.hpp    // 每个模块下,都有一个自定义的初始化公共函数文件
  │  │  └─ ...                      // 自己增加的公共源码文件
  │  ├─ service                     // 服务文件目录
  │  │  ├─ svc_test_query_function.hpp // 服务源码文件,与服务名称同名的 *.hpp 文件
  │  │  └─ ...                      // 其他服务的源码文件
  │  ├─ makefile                    // 模块动态库的 makefile 文件
  │  └─ mdl_test.cpp                // 模块动态库的 main 文件,与模块同名的 *.cpp 文件
  └─ ...
1
2
3
4
5
6
7
8
9
10
11

makefile:自动生成,不需要改动;

mdl_test.cpp:自动生成,不需要改动;

public/f_custom_dll_init.hpp:根据业务需要,修改动态库文件的初始化函数,大多数情况不需要改动;

service/svc_test_query_function.hpp:service 目录下的服务文件,也就是注册服务的代码文件,在这里实现业务逻辑;

其他文件,大多数情况不需要改动,只需关注服务的核心逻辑开发即可。

# 编写服务

我们通过注册服务后生成的服务代码,增加业务逻辑

#pragma once

#include <iostream>
#include <vector>
#include "../../../model/tb_struct.hpp"
#include "../../../utils/OtlV4Helper.hpp"
#include "../../../utils/f_check_user_login.hpp"
#include "../../../../../include/kedao_sync_logs.h"
#include "../../../../../include/kedao_public.h"
#include "../../../../../include/openssl_crypto.h"

using namespace kedao_utils;
using namespace prj_c;

extern "C"
int svc_test_query_function(const string &in_data, string &out_data, const string &request_msg)
{
    int iRst = 0;
    string sql_txt = "";
    vector<string> v_params;
    SVC_REQUEST_OBJ<SYS_FUNCTION> req;                           // 服务的请求参数
    SVC_RESPONSE_OBJ<vector<SYS_FUNCTION>> resp;                 // 服务的返回参数
    OtlV4Helper &dbconn = *((OtlV4Helper *)g_SHARED_PTR->get());

    SVC_REQUEST_MSG svc_request_msg;                             // http请求头部的内容
    JsonHelper::JsonToObject(svc_request_msg, request_msg);      // http请求头部的内容转对象

    try {
        // 设置保持连接,不 logoff(),避免高并发下的数据库不停建立新连接带来的消耗(不同数据库建立连接的耗时在20~200ms不等)
        // 说明:在高并发的情况下,不会重复建立连接,性能提升非常明显;注意:设置为 true 后,即使服务中执行了 dbconn.logoff(),也不会断开数据库连接
        dbconn.set_keep_connection(true);
        // 说明:执行 dbconn.conn() 时,默认自动开启事务
        dbconn.conn();
        const string &req_jsonStr = in_data;
        // 解析入参到对象(将请求参数转换为数据对象)
        if (!aigc::JsonHelper::JsonToObject(req, req_jsonStr))
        {
            // 解包失败
            iRst = -1;
            resp.code = iRst;
            resp.otl_exc.code = iRst;
            resp.err_msg = "服务 svc_test_query_function :解包失败。";
            JsonHelper::ObjectToJson(resp, out_data);
            // 打印错误日志
            sync_cerr << out_data << endl;
            // 断开数据库连接
            dbconn.logoff();
            return iRst;
        }

        // 检查用户登录有效性
        // 函数说明:在账号登录时,产生一个随机字符串 login_key,每次服务请求,都会带着 login_key,与数据库的值进行比较,相等,则表示是有效的请求;不相等则认为是无效的请求。
        if (check_user_login(dbconn, req.sys_head.usr_id, req.sys_head.login_key))
        {
            iRst = -1;
            resp.code = iRst;
            resp.otl_exc.code = iRst;
            resp.err_msg = "服务 svc_test_query_function :用户登录有效性校验失败。";
            JsonHelper::ObjectToJson(resp, out_data);
            // 打印错误日志
            sync_cerr << out_data << endl;
            // 断开数据库连接
            dbconn.logoff();
            return iRst;
        }

        // 开始事务
        // 说明:执行 dbconn.conn() 时,默认自动开启事务,重复执行也不影响事务的开启
        dbconn.begin();

        // ---------------------------------------------------------------------
        // 编写业务逻辑代码
        // ---------------------------------------------------------------------

        /*
        说明:
            otlv4.h 组织SQL语句时参数结构 :参数名<参数类型[字段的长度 + 1]>
            其中:
            1、每个参数以冒号 ":" 标识;
            2、冒号后面跟着参数名称(参数名任意,在同一条语句中不重复即可);
            3、参数名后面用一对尖括号 <> ,尖括号中为传入参数的数据类型,即字段的类型;
            4、常用的参数类型有: char、double、int、long、long long 等等;
            5、如果字符串类型,要指定长度,且长度比字段长度多1(结束符);
            6、如果拼接的 SQL 语句中包含 ":",冒号要进行转义,比如 PostgreSQL 中通过获取 uuid_generate_v4()::text 获取 uuid 时,要写成 uuid_generate_v4()\\:\\:text
        */
        // 组织SQL语句
        if (req.data.menu_title == "")
        {
            sql_txt = "SELECT * FROM sys_function ORDER BY menu_title";
        }
        else
        {
            sql_txt = "SELECT * FROM sys_function WHERE menu_title = :p_01<char[33]>";
        }
        // 组织参数
        v_params.clear();
        v_params.push_back(req.data.menu_title);
        // 声明变量,接收查询结果
        std::vector<SYS_FUNCTION> v_sys_functions;
        // 执行查询
        iRst = dbconn.f_otl_query_to_objs(sql_txt, v_params, v_sys_functions);
        if (iRst)
        {
            resp.code = iRst;
            resp.otl_exc.code = iRst;
            resp.err_msg = "服务[svc_test_query_function]-查询 sys_function 时发生错误";
            JsonHelper::ObjectToJson(resp, out_data);
            // 打印错误日志
            sync_cerr << out_data << endl;
            // 事务回滚
            dbconn.rollback();
            // 断开数据库连接
            dbconn.logoff();
            return iRst;
        }

        /*
            OtlV4Helper 中常用的函数有:
            1、原子服务,实现数据表的增删改查;使用原子服务时,数据表必须有唯一健字段
                atom_exec_delete()
                atom_exec_insert()
                atom_exec_select()
                atom_exec_update()
            2、执行SQL语句,无结果返回
                f_otl_exec()
            3、执行SQL语句,返回单个值(字符串)
                f_otl_query_to_singleValue()
            4、执行SQL语句,返回多条记录容器 vector<>
                f_otl_query_to_objs()
            5、执行SQL语句,返回单条记录的数据对象
                f_otl_query_to_obj()
            6、执行SQL语句,返回多条记录 vector<map<string, string>>
                f_otl_query_to_maps()
            7、执行SQL语句,返回单条记录的 map<string, string>
                f_otl_query_to_map()
        */

        /*
            关于日志输出,日志输出有3种方式,都支持多线程高并发:
            1、日志输出到 stdout,通常用在开发过程中打印日志,用来辅助测试,发布生产环境时,应该删除此类日志的输出
            sync_cout << "日志输出到 bin/stdout 文件" << endl;
            2、错误日志输出到 stderr,通常用在服务发生错误时的日志打印
            sync_cerr << "错误日志输出到 bin/stderr 文件" << endl;
            3、日志输出到 log 日志文件,每天产生一个log日志文件,在 bin/log 目录下;kedao中间件默认输出每一个服务的执行情况
            sync_clog << "日志输出到 bin/log/8位日期_log 文件" << endl;
        */

        // ---------------------------------------------------------------------
        // 将结果赋值给返回对象
        // ---------------------------------------------------------------------
        resp.data = v_sys_functions;
        
        // ---------------------------------------------------------------------
        // 打包返回结果
        // ---------------------------------------------------------------------
        out_data = "";
        resp.code = iRst;
        resp.otl_exc.code = iRst;
        if (!aigc::JsonHelper::ObjectToJson(resp, out_data))      // 将返回的数据对象 resp 转换为 Json 字符串 out_data
        {
            // 打包失败
            iRst = -1;
            resp.code = iRst;
            resp.otl_exc.code = iRst;
            resp.err_msg = "服务 svc_test_query_function :打包失败。";
            JsonHelper::ObjectToJson(resp, out_data);
            // 打印错误日志
            sync_cerr << out_data << endl;
            // 事务回滚
            dbconn.rollback();
            // 断开数据库连接
            dbconn.logoff();
            return iRst;
        }

        // 提交事务
        dbconn.commit();
    }
    catch(otl_exception& p){
        if (p.code == 35 || p.code == 8) { *g_SHARED_PTR = make_shared<OtlV4Helper>(); }
        // 错误码
        iRst = p.code;
        resp.code = iRst;
        // 获取异常数据
        resp.otl_exc.code = p.code;
        resp.err_msg = (char *)p.msg;
        resp.otl_exc.stm_text = p.stm_text;
        resp.otl_exc.sqlstate = (char *)p.sqlstate;
        resp.otl_exc.var_info = p.var_info;
        JsonHelper::ObjectToJson(resp, out_data);
        // 打印错误日志
        sync_cerr << out_data << endl;
        // 事务回滚(如果是查询,出现异常时,这里没有必要进行回滚,否则,会再次触发异常,导致程序终止,前端得不到异常返回的信息)
        dbconn.rollback();
    }

    // 断开数据库连接
    dbconn.logoff();

    return iRst;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201

# 编译服务

在模块目录下执行 make 命令:

make
1

# 动态库的输出路径

在 bin/lib/用户名/系统名 目录下,生成与模块名同名的 *.so 动态库文件。