mysql_service.cpp 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. #include "db/mysql_service.h"
  2. #include <cctype>
  3. #include <exception>
  4. #include <string>
  5. #include <vector>
  6. #include "base/exception.h"
  7. #include "common/logger.h"
  8. #include "util/file.h"
  9. namespace im {
  10. namespace {
  11. std::string trim_sql(const std::string& s)
  12. {
  13. size_t b = 0;
  14. size_t e = s.size();
  15. while (b < e && std::isspace(static_cast<unsigned char>(s[b]))) {
  16. ++b;
  17. }
  18. while (e > b && std::isspace(static_cast<unsigned char>(s[e - 1]))) {
  19. --e;
  20. }
  21. return s.substr(b, e - b);
  22. }
  23. bool starts_ci(const std::string& s, const char* prefix)
  24. {
  25. const size_t n = std::char_traits<char>::length(prefix);
  26. if (s.size() < n) {
  27. return false;
  28. }
  29. for (size_t i = 0; i < n; ++i) {
  30. const unsigned char a = static_cast<unsigned char>(s[i]);
  31. const unsigned char b = static_cast<unsigned char>(prefix[i]);
  32. if (std::tolower(a) != std::tolower(b)) {
  33. return false;
  34. }
  35. }
  36. return true;
  37. }
  38. std::vector<std::string> split_sql(const std::string& sql)
  39. {
  40. std::vector<std::string> out;
  41. std::string cur;
  42. bool in_str = false;
  43. bool in_line = false;
  44. bool in_block = false;
  45. for (size_t i = 0; i < sql.size(); ++i) {
  46. const char c = sql[i];
  47. const char n = (i + 1 < sql.size()) ? sql[i + 1] : '\0';
  48. if (in_line) {
  49. if (c == '\n') {
  50. in_line = false;
  51. }
  52. continue;
  53. }
  54. if (in_block) {
  55. if (c == '*' && n == '/') {
  56. in_block = false;
  57. ++i;
  58. }
  59. continue;
  60. }
  61. if (in_str) {
  62. cur.push_back(c);
  63. if (c == '\\' && n != '\0') {
  64. cur.push_back(n);
  65. ++i;
  66. continue;
  67. }
  68. if (c == '\'') {
  69. if (n == '\'') {
  70. cur.push_back(n);
  71. ++i;
  72. } else {
  73. in_str = false;
  74. }
  75. }
  76. continue;
  77. }
  78. if (c == '-' && n == '-') {
  79. in_line = true;
  80. ++i;
  81. continue;
  82. }
  83. if (c == '/' && n == '*') {
  84. in_block = true;
  85. ++i;
  86. continue;
  87. }
  88. if (c == '\'') {
  89. in_str = true;
  90. cur.push_back(c);
  91. continue;
  92. }
  93. if (c == ';') {
  94. const std::string stmt = trim_sql(cur);
  95. if (!stmt.empty()) {
  96. out.push_back(stmt);
  97. }
  98. cur.clear();
  99. continue;
  100. }
  101. cur.push_back(c);
  102. }
  103. const std::string tail = trim_sql(cur);
  104. if (!tail.empty()) {
  105. out.push_back(tail);
  106. }
  107. return out;
  108. }
  109. bool exec_sql(ylib::mysql::conn* conn, const std::string& sql)
  110. {
  111. auto* stmt = conn->setsql(sql);
  112. if (stmt == nullptr) {
  113. Logger::error("mysql exec prepare failed: " + sql.substr(0, 96));
  114. return false;
  115. }
  116. stmt->update();
  117. return true;
  118. }
  119. } // namespace
  120. MysqlConn::MysqlConn(ylib::mysql::conn* conn) : conn_(conn) {}
  121. MysqlConn::MysqlConn(MysqlConn&& other) noexcept : conn_(other.conn_)
  122. {
  123. other.conn_ = nullptr;
  124. }
  125. MysqlConn& MysqlConn::operator=(MysqlConn&& other) noexcept
  126. {
  127. if (this != &other) {
  128. release();
  129. conn_ = other.conn_;
  130. other.conn_ = nullptr;
  131. }
  132. return *this;
  133. }
  134. MysqlConn::~MysqlConn()
  135. {
  136. release();
  137. }
  138. void MysqlConn::release()
  139. {
  140. if (conn_ == nullptr) {
  141. return;
  142. }
  143. if (conn_->pool() != nullptr) {
  144. conn_->pool()->recover(conn_);
  145. }
  146. conn_ = nullptr;
  147. }
  148. bool MysqlService::start(const MysqlConfig& cfg)
  149. {
  150. ylib::mysql::mysql_conn_info info;
  151. info.ipaddress = cfg.host;
  152. info.port = cfg.port;
  153. info.username = cfg.user;
  154. info.password = cfg.password;
  155. info.database = cfg.database;
  156. info.charset = cfg.charset;
  157. if (!pool_.start(info, cfg.pool_size)) {
  158. Logger::error("mysql pool start failed");
  159. return false;
  160. }
  161. started_.store(true, std::memory_order_release);
  162. if (!ping()) {
  163. started_.store(false, std::memory_order_release);
  164. pool_.close();
  165. Logger::error("mysql ping failed, check host/user/password/database");
  166. return false;
  167. }
  168. Logger::info("mysql pool ready, size=" + std::to_string(cfg.pool_size) +
  169. " db=" + cfg.database + "@" + cfg.host + ":" + std::to_string(cfg.port));
  170. ensure_schema();
  171. return true;
  172. }
  173. bool MysqlService::ensure_schema()
  174. {
  175. const char* stmts[] = {
  176. "ALTER TABLE im_user ADD COLUMN allow_search_account TINYINT NOT NULL DEFAULT 1",
  177. "ALTER TABLE im_user ADD COLUMN allow_search_phone TINYINT NOT NULL DEFAULT 1",
  178. "ALTER TABLE im_user ADD COLUMN allow_add_friend TINYINT NOT NULL DEFAULT 1",
  179. "ALTER TABLE im_friend_request ADD COLUMN message VARCHAR(64) NOT NULL DEFAULT ''",
  180. "ALTER TABLE im_friend ADD COLUMN remark VARCHAR(64) NOT NULL DEFAULT ''",
  181. "ALTER TABLE im_user ADD COLUMN avatar_ver INT NOT NULL DEFAULT 0",
  182. "ALTER TABLE im_group ADD COLUMN avatar_ver INT NOT NULL DEFAULT 0",
  183. "ALTER TABLE im_user ADD COLUMN file_max BIGINT NOT NULL DEFAULT 0",
  184. "ALTER TABLE im_message ADD COLUMN sender_deleted TINYINT NOT NULL DEFAULT 0",
  185. "ALTER TABLE im_message ADD COLUMN receiver_deleted TINYINT NOT NULL DEFAULT 0",
  186. "CREATE TABLE IF NOT EXISTS im_friend_request ("
  187. "id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,"
  188. "from_uid BIGINT UNSIGNED NOT NULL,"
  189. "to_uid BIGINT UNSIGNED NOT NULL,"
  190. "status TINYINT NOT NULL DEFAULT 0,"
  191. "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,"
  192. "updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,"
  193. "PRIMARY KEY (id), UNIQUE KEY uk_from_to (from_uid, to_uid),"
  194. "KEY idx_to_status (to_uid, status)"
  195. ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
  196. "CREATE TABLE IF NOT EXISTS im_sticker_pack ("
  197. "id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,"
  198. "name VARCHAR(64) NOT NULL,"
  199. "version INT NOT NULL DEFAULT 1,"
  200. "sort_order INT NOT NULL DEFAULT 0,"
  201. "status TINYINT NOT NULL DEFAULT 1,"
  202. "PRIMARY KEY (id)"
  203. ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
  204. "CREATE TABLE IF NOT EXISTS im_sticker ("
  205. "id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,"
  206. "pack_id BIGINT UNSIGNED NOT NULL,"
  207. "code VARCHAR(64) NOT NULL,"
  208. "name VARCHAR(64) NOT NULL DEFAULT '',"
  209. "file_path VARCHAR(255) NOT NULL,"
  210. "sha256 VARCHAR(64) NOT NULL DEFAULT '',"
  211. "mime VARCHAR(32) NOT NULL DEFAULT 'image/png',"
  212. "sort_order INT NOT NULL DEFAULT 0,"
  213. "PRIMARY KEY (id), UNIQUE KEY uk_pack_code (pack_id, code), KEY idx_pack (pack_id)"
  214. ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
  215. "CREATE TABLE IF NOT EXISTS im_file ("
  216. "id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,"
  217. "from_uid BIGINT UNSIGNED NOT NULL,"
  218. "name VARCHAR(255) NOT NULL,"
  219. "mime VARCHAR(64) NOT NULL DEFAULT 'application/octet-stream',"
  220. "size INT NOT NULL DEFAULT 0,"
  221. "sha256 VARCHAR(64) NOT NULL DEFAULT '',"
  222. "file_path VARCHAR(255) NOT NULL DEFAULT '',"
  223. "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,"
  224. "PRIMARY KEY (id), KEY idx_from (from_uid)"
  225. ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
  226. "CREATE TABLE IF NOT EXISTS im_group ("
  227. "id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,"
  228. "name VARCHAR(64) NOT NULL DEFAULT '',"
  229. "owner_uid BIGINT UNSIGNED NOT NULL,"
  230. "join_mode TINYINT NOT NULL DEFAULT 1,"
  231. "member_count INT NOT NULL DEFAULT 0,"
  232. "avatar_ver INT NOT NULL DEFAULT 0,"
  233. "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,"
  234. "updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,"
  235. "PRIMARY KEY (id), KEY idx_owner (owner_uid)"
  236. ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
  237. "CREATE TABLE IF NOT EXISTS im_group_member ("
  238. "group_id BIGINT UNSIGNED NOT NULL,"
  239. "uid BIGINT UNSIGNED NOT NULL,"
  240. "role TINYINT NOT NULL DEFAULT 0,"
  241. "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,"
  242. "PRIMARY KEY (group_id, uid), KEY idx_uid (uid)"
  243. ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
  244. "CREATE TABLE IF NOT EXISTS im_group_request ("
  245. "id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,"
  246. "group_id BIGINT UNSIGNED NOT NULL,"
  247. "from_uid BIGINT UNSIGNED NOT NULL,"
  248. "status TINYINT NOT NULL DEFAULT 0,"
  249. "message VARCHAR(64) NOT NULL DEFAULT '',"
  250. "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,"
  251. "updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,"
  252. "PRIMARY KEY (id), UNIQUE KEY uk_group_from (group_id, from_uid),"
  253. "KEY idx_group_status (group_id, status)"
  254. ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
  255. "CREATE TABLE IF NOT EXISTS im_group_message ("
  256. "id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,"
  257. "group_id BIGINT UNSIGNED NOT NULL,"
  258. "from_uid BIGINT UNSIGNED NOT NULL DEFAULT 0,"
  259. "msg_type TINYINT NOT NULL DEFAULT 1,"
  260. "content TEXT NOT NULL,"
  261. "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,"
  262. "PRIMARY KEY (id), KEY idx_group_id (group_id, id)"
  263. ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
  264. "CREATE TABLE IF NOT EXISTS im_group_msg_deleted ("
  265. "uid BIGINT UNSIGNED NOT NULL,"
  266. "msg_id BIGINT UNSIGNED NOT NULL,"
  267. "PRIMARY KEY (uid, msg_id), KEY idx_msg (msg_id)"
  268. ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
  269. "CREATE TABLE IF NOT EXISTS im_group_inbox ("
  270. "uid BIGINT UNSIGNED NOT NULL,"
  271. "group_id BIGINT UNSIGNED NOT NULL,"
  272. "last_msg_id BIGINT UNSIGNED NOT NULL DEFAULT 0,"
  273. "last_content VARCHAR(256) NOT NULL DEFAULT '',"
  274. "last_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,"
  275. "unread INT NOT NULL DEFAULT 0,"
  276. "PRIMARY KEY (uid, group_id), KEY idx_uid_time (uid, last_time)"
  277. ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
  278. };
  279. try {
  280. auto conn = acquire();
  281. for (const char* sql : stmts) {
  282. try {
  283. exec_sql(conn.get(), sql);
  284. } catch (const std::exception& e) {
  285. Logger::info(std::string("ensure_schema skip: ") + e.what());
  286. }
  287. }
  288. return true;
  289. } catch (const std::exception& e) {
  290. Logger::error(std::string("ensure_schema: ") + e.what());
  291. return false;
  292. }
  293. }
  294. bool MysqlService::init_schema(const MysqlConfig& cfg, const std::string& sql_path)
  295. {
  296. ylib::buffer raw = ylib::file::read(sql_path);
  297. std::string sql = raw.to_string();
  298. if (sql.size() >= 3 && static_cast<unsigned char>(sql[0]) == 0xEF &&
  299. static_cast<unsigned char>(sql[1]) == 0xBB &&
  300. static_cast<unsigned char>(sql[2]) == 0xBF) {
  301. sql.erase(0, 3);
  302. }
  303. if (trim_sql(sql).empty()) {
  304. Logger::error("init sql empty: " + sql_path);
  305. return false;
  306. }
  307. ylib::mysql::mysql_conn_info info;
  308. info.ipaddress = cfg.host;
  309. info.port = cfg.port;
  310. info.username = cfg.user;
  311. info.password = cfg.password;
  312. info.charset = cfg.charset;
  313. info.database = "";
  314. ylib::mysql::pool tmp;
  315. if (!tmp.start(info, 1)) {
  316. tmp.close();
  317. info.database = cfg.database;
  318. if (!tmp.start(info, 1)) {
  319. Logger::error("mysql init connect failed");
  320. return false;
  321. }
  322. }
  323. bool ok = false;
  324. try {
  325. MysqlConn conn(tmp.get());
  326. if (!conn) {
  327. Logger::error("mysql init acquire failed");
  328. } else {
  329. const std::string create_db =
  330. "CREATE DATABASE IF NOT EXISTS `" + cfg.database +
  331. "` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci";
  332. try {
  333. if (!exec_sql(conn.get(), create_db)) {
  334. Logger::warn("create database skipped, try existing db=" + cfg.database);
  335. }
  336. } catch (const std::exception& e) {
  337. Logger::warn(std::string("create database skipped: ") + e.what());
  338. }
  339. conn->setDatabase(cfg.database);
  340. ok = true;
  341. for (const auto& stmt : split_sql(sql)) {
  342. if (starts_ci(stmt, "USE ") || starts_ci(stmt, "CREATE DATABASE")) {
  343. continue;
  344. }
  345. if (!exec_sql(conn.get(), stmt)) {
  346. ok = false;
  347. break;
  348. }
  349. }
  350. }
  351. } catch (const std::exception& e) {
  352. Logger::error(std::string("mysql init schema failed: ") + e.what());
  353. ok = false;
  354. }
  355. tmp.close();
  356. if (!ok) {
  357. return false;
  358. }
  359. Logger::info("mysql schema ready, db=" + cfg.database + " sql=" + sql_path);
  360. return true;
  361. }
  362. void MysqlService::stop()
  363. {
  364. if (!started_.load(std::memory_order_acquire)) {
  365. return;
  366. }
  367. pool_.close();
  368. started_.store(false, std::memory_order_release);
  369. Logger::info("mysql pool stopped");
  370. }
  371. MysqlConn MysqlService::acquire()
  372. {
  373. if (!started_.load(std::memory_order_acquire)) {
  374. throw ylib::exception("mysql pool is not started");
  375. }
  376. return MysqlConn(pool_.get());
  377. }
  378. bool MysqlService::ping()
  379. {
  380. try {
  381. auto conn = acquire();
  382. auto* stmt = conn->setsql("SELECT 1");
  383. if (stmt == nullptr) {
  384. return false;
  385. }
  386. ylib::mysql::result* rs = stmt->query();
  387. return rs != nullptr;
  388. } catch (const std::exception& e) {
  389. Logger::error(std::string("mysql ping exception: ") + e.what());
  390. return false;
  391. }
  392. }
  393. } // namespace im