服务网站开发,广州海珠区有什么大学,wordpress一直加载,网站的模糊搜索怎么做文章目录 一、图床项目介绍二、图床项目架构三、图床功能实现3.1 注册功能3.2 登录功能3.3 用户文件列表3.4 上传文件3.5 上传文件之秒传3.6 获取共享文件列表或下载榜3.7 分享/ 删除文件/ 更新下载数3.8 取消分享/ 转存/ 更新下载计数3.9 图床分享图片 一、图床项目介绍
实现… 文章目录 一、图床项目介绍二、图床项目架构三、图床功能实现3.1 注册功能3.2 登录功能3.3 用户文件列表3.4 上传文件3.5 上传文件之秒传3.6 获取共享文件列表或下载榜3.7 分享/ 删除文件/ 更新下载数3.8 取消分享/ 转存/ 更新下载计数3.9 图床分享图片 一、图床项目介绍
实现一个能够上传、存储、分享图片的后端项目。
1上传上传文件并且如果上传的文件在数据库中有记录即md5匹配则实现秒传效果。 2分享共享文件共享文件给其他已注册的用户。其他注册用户可以在 “共享文件–文件列表” 中看到共享的文件并且可转存到自己的文件列表或者下载。同样在自己的 “共享文件–文件列表”中可以查看共享文件的信息也可以取消共享若取消共享除非其他用户已经转存否则就看不到。 3分享图片生成链接其他未注册用户可以根据链接查看已分享的图片可在 “共享文件 --我的共享图片” 中看到相关浏览信息也可以取消分享。
二、图床项目架构
文件上传逻辑 客户端上传图片 ⟹ \Longrightarrow ⟹nginx代理 ⟹ \Longrightarrow ⟹通过nginx-upload-module上传到某个临时目录 ⟹ \Longrightarrow ⟹透传到后端服务程序tc-http-server ⟹ \Longrightarrow ⟹reactor网络模型监听到任务解析http请求然后将任务交由线程池处理 ⟹ \Longrightarrow ⟹把文件信息存储到数据库同时把文件上传到fastdfs 主要的http api接口 reactor网络模型
三、图床功能实现
3.1 注册功能 // 回发信息给前端的格式
#define HTTP_RESPONSE_HTML \HTTP/1.1 200 OK\r\n \Connection:close\r\n \Content-Length:%d\r\n \Content-Type:application/json;charsetutf-8\r\n\r\n%s// 注册函数
int ApiRegisterUser(uint32_t conn_uuid, string url, string post_data) {string str_json;UNUSED(url);int ret 0;string user_name;string nick_name;string pwd;string phone;string email;LogInfo(uuid: {}, url: {}, post_data: {}, conn_uuid, url, post_data);// 1、判断数据是否为空if (post_data.empty()) {LogError(decodeRegisterJson failed);encodeRegisterJson(1, str_json);ret -1;goto END;}// 2、解析jsonif (decodeRegisterJson(post_data, user_name, nick_name, pwd, phone, email) 0) {LogError(decodeRegisterJson failed);encodeRegisterJson(1, str_json);ret -1;goto END;}// 3、注册账号ret registerUser(user_name, nick_name, pwd, phone, email);ret encodeRegisterJson(ret, str_json);// 这里是裸数据// 发送到回发队列里
END:// 3、把状态结果按回复消息的格式打包char *str_content new char[HTTP_RESPONSE_HTML_MAX];uint32_t ulen str_json.length();snprintf(str_content, HTTP_RESPONSE_HTML_MAX, HTTP_RESPONSE_HTML, ulen,str_json.c_str());str_json str_content;// 4、添加到回发队列CHttpConn::AddResponseData(conn_uuid, str_json);delete str_content;return ret;
}重点看一下registerUser(user_name, nick_name, pwd, phone, email);的处理过程先查看用户是否存在存在就返回不存在需要就把用户信息添加到数据库完成注册。
int registerUser(string user_name, string nick_name, string pwd,string phone, string email) {int ret 0;uint32_t user_id;// 1、获取数据库连接池CDBManager *db_manager CDBManager::getInstance();CDBConn *db_conn db_manager-GetDBConn(tuchuang_slave);AUTO_REL_DBCONN(db_manager, db_conn);// 2、先查看用户是否存在string str_sql;str_sql formatString2(select * from user_info where user_name%s,user_name.c_str());CResultSet *result_set db_conn-ExecuteQuery(str_sql.c_str()); // 执行sql语句if (result_set result_set-Next()) { // 2.1 存在用户记录返回LogWarn(id: {}, user_name: {} 已经存在, result_set-GetInt(id), result_set-GetString(user_name));delete result_set;ret 2;} else { // 2.2 如果不存在注册time_t now;char create_time[TIME_STRING_LEN];//获取当前时间now time(NULL);strftime(create_time, TIME_STRING_LEN - 1, %Y-%m-%d %H:%M:%S,localtime(now));// 向数据库插入信息的语句str_sql insert into user_info (user_name,nick_name,password,phone,email,create_time) values(?,?,?,?,?,?);LogInfo(执行: {}, str_sql);CPrepareStatement *stmt new CPrepareStatement();if (stmt-Init(db_conn-GetMysql(), str_sql)) {uint32_t index 0;string c_time create_time;stmt-SetParam(index, user_name);stmt-SetParam(index, nick_name);stmt-SetParam(index, pwd);stmt-SetParam(index, phone);stmt-SetParam(index, email);stmt-SetParam(index, c_time);bool bRet stmt-ExecuteUpdate();if (bRet) {ret 0;user_id db_conn-GetInsertId();LogInfo(insert user_id: {}, user_id);} else {LogError(insert user_info failed. {}, str_sql);ret 1;}}delete stmt;}return ret;
}3.2 登录功能 int ApiUserLogin(u_int32_t conn_uuid, std::string url, std::string post_data)
{UNUSED(url);string user_name;string pwd;string token;string str_json;// 1、判断数据是否为空if (post_data.empty()) {encodeLoginJson(1, token, str_json);goto END;}// 2、解析jsonif (decodeLoginJson(post_data, user_name, pwd) 0) {LogError(decodeRegisterJson failed);encodeLoginJson(1, token, str_json);goto END;}// 3、验证账号和密码是否匹配if (verifyUserPassword(user_name, pwd) 0) {LogError(verifyUserPassword failed);encodeLoginJson(1, token, str_json);goto END;}// 4、生成token并存储到redis中if (setToken(user_name, token) 0) {LogError(setToken failed);encodeLoginJson(1, token, str_json);goto END;}// 5、加载 我的文件数量 我的分享图片数量if (loadMyfilesCountAndSharepictureCount(user_name) 0) {LogError(loadMyfilesCountAndSharepictureCount failed);encodeLoginJson(1, token, str_json);goto END;}encodeLoginJson(0, token, str_json);
END:char *str_content new char[HTTP_RESPONSE_HTML_MAX];uint32_t ulen str_json.length();snprintf(str_content, HTTP_RESPONSE_HTML_MAX, HTTP_RESPONSE_HTML, ulen,str_json.c_str());str_json str_content;CHttpConn::AddResponseData(conn_uuid, str_json);delete str_content;return 0;
}关注一下三个过程 verifyUserPassword(user_name, pwd)验证账号密码是否匹配。
setToken(user_name, token)生成token并存储到redis中。所谓token相当于令牌前面账号密码验证过后说明你是有户口的人放你进来。但是你在访问其他功能的时候需要有个通关令牌一个只有服务器和客户端前端知道这个字符串来再次验证你的身份而不用每次都通过账号密码。于是 Token 就成了这两者之间的密钥它可以让服务器确认请求是来自客户端还是恶意的第三方。
int setToken(string user_name, string token) {int ret 0;CacheManager *cache_manager CacheManager::getInstance();CacheConn *cache_conn cache_manager-GetCacheConn(token);AUTO_REL_CACHECONN(cache_manager, cache_conn);token RandomString(32); // 随机32个字母if (cache_conn) {//用户名token, 86400有效时间为24小时cache_conn-SetEx(user_name, 86400, token); // redis做超时} else {ret -1;}return ret;
}loadMyfilesCountAndSharepictureCount(user_name) 加载 我的文件数量 和 我的分享图片数量
int loadMyfilesCountAndSharepictureCount(string user_name) {int64_t redis_file_count 0;int mysq_file_count 0;// 1. 获取mysql 连接池CDBManager *db_manager CDBManager::getInstance();CDBConn *db_conn db_manager-GetDBConn(tuchuang_slave);AUTO_REL_DBCONN(db_manager, db_conn);// 2. 获取redis 连接池CacheManager *cache_manager CacheManager::getInstance();CacheConn *cache_conn cache_manager-GetCacheConn(token);AUTO_REL_CACHECONN(cache_manager, cache_conn);// 3. 从mysql加载 用户文件个数if (DBGetUserFilesCountByUsername(db_conn, user_name, mysq_file_count) 0) {LogError(DBGetUserFilesCountByUsername failed);return -1;}// 4. 存储到redisredis_file_count (int64_t)mysq_file_count;if (CacheSetCount(cache_conn, FILE_USER_COUNT user_name,redis_file_count) 0) // 失败了那下次继续从mysql加载{LogError(DBGetUserFilesCountByUsername failed);return -1;}LogInfo(FILE_USER_COUNT: {}, redis_file_count);// 5. 从mysql加载 我的分享图片数量if (DBGetSharePictureCountByUsername(db_conn, user_name, mysq_file_count) 0) {LogError(DBGetUserFilesCountByUsername failed);return -1;}// 6. 存储到redisredis_file_count (int64_t)mysq_file_count;if (CacheSetCount(cache_conn, SHARE_PIC_COUNT user_name,redis_file_count) 0) // 失败了那下次继续从mysql加载{LogError(DBGetUserFilesCountByUsername failed);return -1;}LogInfo(SHARE_PIC_COUNT: {}, redis_file_count);return 0;
}3.3 用户文件列表
查看我的文件时候显示的是图片信息。从浏览器的抓包直观看到我们请求的两个命令 myfiles?cmdcount文件数量 myfiles?cmdnormal文件列表 当然我们还有按排序 /api/myfilescmdpvasc 文件列表( ( 按下载量升序) ) /api/myfilescmd pvdesc 文件列表( ( 按下载量降序) )
int ApiMyfiles(string url, string post_data, string str_json) {// 解析url有没有命令// count 获取用户文件个数// display 获取用户文件信息展示到前端char cmd[20];string user_name;string token;int ret 0;int start 0; //文件起点int count 0; //文件个数//1、解析命令 解析url获取自定义参数QueryParseKeyValue(url.c_str(), cmd, cmd, NULL);LogInfo(url: {}, cmd: {} ,url, cmd);if (strcmp(cmd, count) 0) { // 2. cmd count 获取文件数量// 2.1 解析jsonif (decodeCountJson(post_data, user_name, token) 0) {encodeCountJson(1, 0, str_json);LogError(decodeCountJson failed);return -1;}//2.2 验证登陆token成功返回0失败-1ret VerifyToken(user_name, token); // util_cgi.hif (ret 0) {// 2.3 获取文件数量if (handleUserFilesCount(user_name, count) 0) { //获取用户文件个数LogError(handleUserFilesCount failed);encodeCountJson(1, 0, str_json);} else {LogInfo(handleUserFilesCount ok, count: {}, count);encodeCountJson(0, count, str_json);}} else {LogError(VerifyToken failed);encodeCountJson(1, 0, str_json);}return 0;} else { // 3. cmd normal 或者 ‘pvdesc’ 或者 ‘pvasc’ 获取文件列表if ((strcmp(cmd, normal) ! 0) (strcmp(cmd, pvasc) ! 0) (strcmp(cmd, pvdesc) ! 0)) {LogError(unknow cmd: {}, cmd);encodeCountJson(1, 0, str_json);}// 3.1 通过json包获取信息ret decodeFileslistJson(post_data, user_name, token, start,count);LogInfo(user_name: {}, token:{}, start: {}, count:, user_name,token, start, count);if (ret 0) {// 3.2 验证登陆token成功返回0失败-1ret VerifyToken(user_name, token); // util_cgi.hif (ret 0) {string str_cmd cmd;// 3.3 获取用户文件列表if (getUserFileList(str_cmd, user_name, start, count,str_json) 0) { LogError(getUserFileList failed);encodeCountJson(1, 0, str_json);}} else {LogError(VerifyToken failed);encodeCountJson(1, 0, str_json);}} else {LogError(decodeFileslistJson failed);encodeCountJson(1, 0, str_json);}}return 0;
}1、myfiles?cmdcount文件数量 需要注意获取文件数量我们是先从redis获取如果redis没有再从MySQL获取。如果MySQL有从MySQL获取并把数据写入redis。如果MySQL也没有就报错。
int handleUserFilesCount(string user_name, int count) {CDBManager *db_manager CDBManager::getInstance();CDBConn *db_conn db_manager-GetDBConn(tuchuang_slave);AUTO_REL_DBCONN(db_manager, db_conn);CacheManager *cache_manager CacheManager::getInstance();CacheConn *cache_conn cache_manager-GetCacheConn(token);AUTO_REL_CACHECONN(cache_manager, cache_conn);int ret getUserFilesCount(db_conn, cache_conn, user_name, count);return ret;
}int getUserFilesCount(CDBConn *db_conn, CacheConn *cache_conn,string user_name, int count) {int ret 0;int64_t file_count 0;// 先查看用户是否存在string str_sql;// 1. 先从redis里面获取if (CacheGetCount(cache_conn, FILE_USER_COUNT user_name, file_count) 0) {LogWarn(CacheGetCount failed); // 有可能是因为没有key不要急于判断为错误file_count 0;ret -1;}// 2. redis没有从mysql获取。若MySQL获取到再写入redisif (file_count 0) {// 2.1 从mysql加载count 0;if (DBGetUserFilesCountByUsername(db_conn, user_name, count) 0) { // 如果MySQL也没有就报错LogError(DBGetUserFilesCountByUsername failed);return -1;}// 2.2 将获取的数据写入redisfile_count (int64_t)count;if (CacheSetCount(cache_conn, FILE_USER_COUNT user_name, file_count) 0) {LogError(CacheSetCount failed);return -1;}}count file_count;return ret;
}2、myfiles?cmdnormal文件列表 这是我们后端程序返回的结果前端根据这些字段解析展现
{code: 0,count: 3,files: [{create_time: 2023-08-29 06:45:34,file_name: 黄山景区高清地图.jpg,md5: 825a70d2c0132eca6afe84694c984120,pv: 1,share_status: 1,size: 875885,type: jpg,url: http://192.168.3.128:80/group1/M00/00/00/wKgDgGTtlA6AYQyyAA1dbSSfUFk261.jpg,user: handsome1}],total: 1
}“code”: 0 正常1 失败 “count”: 返回的当前文件数量比如 2 “total”: 个人文件总共的数量 “user”: 用户名称, “md5”: md5 值, “create_time”: 创建时间, “file_name”: 文件名, “share_status”: 共享状态, 0 为没有共享 1 为共享 “pv”: 文件下载量下载一次加 1 “url”: URL, “size”: 文件大小, “type”: 文件类型 /api/myfilescmdpvasc 文件列表( ( 按下载量升序) ) ------ 升序order by pv asc /api/myfilescmd pvdesc 文件列表( ( 按下载量降序) ) ----- 降序order by pv desc 这两个和normal一样只是sql语句中的查询方式不一样。
getUserFileList()函数的大概就是获取连接池然后编写sql执行语句然后交由连接池db_conn-ExecuteQuery(str_sql.c_str())执行最后根据结果解析。这就不再赘述都差不多。主要看看解析成json打包的过程 LogInfo(执行: {}, str_sql);CResultSet *result_set db_conn-ExecuteQuery(str_sql.c_str());if (result_set) {// 遍历所有的内容// 获取大小int file_index 0;Json::Value root, files;root[code] 0;while (result_set-Next()) {Json::Value file;file[user] result_set-GetString(user);file[md5] result_set-GetString(md5);file[create_time] result_set-GetString(create_time);file[file_name] result_set-GetString(file_name);file[share_status] result_set-GetInt(shared_status);file[pv] result_set-GetInt(pv);file[url] result_set-GetString(url);file[size] result_set-GetInt(size);file[type] result_set-GetString(type);files[file_index] file;file_index;}root[files] files;root[count] file_index;root[total] total;Json::FastWriter writer;str_json writer.write(root);delete result_set;return 0;} else {LogError({} 操作失败, str_sql);return -1;}对于result_set-GetString(user)。在数据库连接池设计中我们获取到一行数据我们将列名和列数插入到map中。后续我们可以根据要获取的字段名得到列数再到结果集查找具体数据。 举个例子map里面有user,1、md5,2。通过_GetIndex(user)可知道user字段的数据在结果集的第一列然后通过row_[1]获取结果集row_的第一个也就是user对应的数据。
char *CResultSet::GetString(const char *key) {int idx _GetIndex(key);if (idx -1) {return NULL;} else {return row_[idx]; // 列}
}3.4 上传文件
先介绍一下md5每个文件都有一个唯一的 MD5 值比如2bf8170b42cc7124b04a8886c83a9c6f就好比每个人的指纹都是唯一的一样效验 MD5 就是用来确保文件在传输过程中未被修改过。也就是说如果要上传文件的MD5和数据库的某个文件的MD5匹配意味着这两个文件一样。那么就无需重复上传。
1客户端在上传文件之前将文件的 MD5 码上传到服务器。 2服务器端判断是否已存在此 MD5 码如果存在说明该文件已存在则此文件无需再上传在此文件的计数器加 1说明此文件多了一个用户共用。 3如果服务器没有此 MD5 码说明上传的文件是新文件则真正上传此文件。
我们先将处理上传新文件的逻辑。 1先通过 nginx-upload-module 模块上传文件到临时目录 2nginx-upload-module 模块上传完文件后通知/api/upload 后端处理程序 3后端处理程序 ApiUpload 函数解析文件信息然后将临时文件上传到 fastdfs 1、解析客户端的post请求
------WebKitFormBoundaryLheXCMpLubcS8BsC
Content-Disposition: form-data; namefile; filename牛牛.png
Content-Type: image/png------WebKitFormBoundaryLheXCMpLubcS8BsC
Content-Disposition: form-data; nameuserhandsome1
------WebKitFormBoundaryLheXCMpLubcS8BsC
Content-Disposition: form-data; namemd5aa3a04152a85412779357dc008d67ae7
------WebKitFormBoundaryLheXCMpLubcS8BsC
Content-Disposition: form-data; namesize2292609
------WebKitFormBoundaryLheXCMpLubcS8BsC--post请求通过nginx-upload-module加工后到达后台server后台server逐步从post请求中解析出相应的文件信息 // 1. 解析post请求 // boundary----WebKitFormBoundaryjWE3qXXORSg2hZiB 找到起始位置p1 strstr(begin, \r\n); // 作用是返回字符串中首次出现子串的地址if (p1 NULL) {LogError(wrong no boundary!);ret -1;goto END;}//拷贝分界线strncpy(boundary, begin, p1 - begin); // 缓存分界线, 比如WebKitFormBoundary88asdgewtgewxboundary[p1 - begin] \0; //字符串结束符LogInfo(boundary: {}, boundary); //打印出来// 查找文件名file_namebegin p1 2; // 2-\r\n p2 strstr(begin, name\file_name\); //找到file_name字段if (!p2) {LogError(wrong no file_name!);ret -1;goto END;}p2 strstr(begin, \r\n); // 找到file_name下一行p2 4; //下一行起始begin p2; // p2 strstr(begin, \r\n);strncpy(file_name, begin, p2 - begin);LogInfo(file_name: {}, file_name);// 其他的类似// 查找文件类型file_content_type// ……// 查找文件file_path// ……// 查找文件file_md5// ……// 查找文件file_size// ……// 查找user// ……2、根据文件后缀对临时文件做重命名 // 2. 根据文件后缀对临时文件做重命名 // 获取文件名后缀GetFileSuffix(file_name, suffix); // 20230720-2.txt - txt mp4, jpg, pngstrcat(new_file_path, file_path); // /root/tmp/1/0045118901strcat(new_file_path, .); // /root/tmp/1/0045118901.strcat(new_file_path, suffix); // /root/tmp/1/0045118901.txt// 重命名 修改文件名ret rename(file_path, new_file_path); /// /root/tmp/1/0045118901 - /root/tmp/1/0045118901.txtif (ret 0) {LogError(rename {} to {} failed, file_path, new_file_path);ret -1;goto END;} 3、将该文件存入fastDFS中,并得到文件的file_id // 3. 将该文件存入fastDFS中,并得到文件的file_id // file_id 例如 group1/M00/00/00/ctepQmIWLzWAHzHrAAAAKTIQHvk745.txtLogInfo(uploadFileToFastDfs, file_name:{}, new_file_path:{}, file_name, new_file_path);if (uploadFileToFastDfs(new_file_path, fileid) 0) {LogError(uploadFileToFastDfs failed, unlink: {}, new_file_path);ret unlink(new_file_path);if (ret ! 0) {LogError(unlink: {} failed, new_file_path); // 删除失败则需要有个监控重新清除过期的临时文件比如过期两天的都删除}ret -1;goto END;}将这个本地文件上传到 后台分布式文件系统fastdfs中具体来说通过多进程的方式子进程通过execlp进程替换执行fastdfs写的的客户端上传文件的程序
//fdfs_upload_file 客户端的配置文件(/etc/fdfs/client.conf) 要上传的文件
fdfs_upload_file /etc/fdfs/client.conf zxm.txt/* -------------------------------------------*/
/*** brief 将一个本地文件上传到 后台分布式文件系统中* 对应 fdfs_upload_file /etc/fdfs/client.conf 完整文件路径** param file_path (in) 本地文件的路径* param fileid (out)得到上传之后的文件ID路径** returns* 0 succ, -1 fail*/
/* -------------------------------------------*/
int uploadFileToFastDfs(char *file_path, char *fileid) {int ret 0;pid_t pid;int fd[2];//无名管道的创建if (pipe(fd) 0) // fd[0] → r fd[1] → w 获取上传后返回的信息 fileid{LogError(pipe error);ret -1;goto END;}//创建进程pid fork(); // if (pid 0) //进程创建失败{LogError(fork error);ret -1;goto END;}if (pid 0) //子进程{//关闭读端close(fd[0]);//将标准输出 重定向 写管道dup2(fd[1],STDOUT_FILENO); // 往标准输出写的东西都会重定向到fd所指向的文件,// 当fileid产生时输出到管道fd[1]// fdfs_upload_file /etc/fdfs/client.conf 123.txt//通过execlp执行fdfs_upload_file//如果函数调用成功,进程自己的执行代码就会变成加载程序的代码,execlp()后边的代码也就不会执行了.execlp(fdfs_upload_file, fdfs_upload_file,s_dfs_path_client.c_str(), file_path, NULL); //// 执行正常不会跑下面的代码//执行失败LogError(execlp fdfs_upload_file error);close(fd[1]);} else //父进程{//关闭写端close(fd[1]);//从管道中去读数据read(fd[0], fileid, TEMP_BUF_MAX_LEN); // 等待管道写入然后读取LogInfo(fileid1: {}, fileid);//去掉一个字符串两边的空白字符TrimSpace(fileid);if (strlen(fileid) 0) {LogError(upload failed);ret -1;goto END;}LogInfo(fileid2: {}, fileid);wait(NULL); //等待子进程结束回收其资源close(fd[0]);}END:return ret;
}4、删除本地临时存放的上传文件 // 4. 删除本地临时存放的上传文件 LogInfo(unlink: {}, new_file_path);ret unlink(new_file_path);if (ret ! 0) {LogWarn(unlink: {} failed, new_file_path); // 删除失败则需要有个监控重新清除过期的临时文件比如过期两天的都删除}5、得到文件所存放storage的host_name拼接出完整的http地址 // 5. 得到文件所存放storage的host_name // 拼接出完整的http地址LogInfo(getFullurlByFileid, fileid: {}, fileid);if (getFullurlByFileid(fileid, fdfs_file_url) 0) {LogError(getFullurlByFileid failed );ret -1;goto END;}和把文件上传到fastdfs系统一样都是多进程加管道通信
// 子进程
//将标准输出 重定向 写管道
dup2(fd[1], STDOUT_FILENO);
/*读取存储文件的信息文件利用fastdfs自带的fdfs_file_info进程*/
//使用“fdfs_file_info”可以查看到文件的详细存储信息也是跟上客户端的配置文件以及储服务器返回给我们的文件的路径execlp(fdfs_file_info, fdfs_file_info, fdfs_cli_conf_path, fileid, NULL);// 父进程
//从管道中去读数据
read(fd[0], fdfs_file_stat_buf, TEMP_BUF_MAX_LEN);
//拼接上传文件的完整url地址---http://host_name/group1/M00/00/00/D12313123232312.png6、将该文件的FastDFS相关信息存入mysql中 // 将该文件的FastDFS相关信息存入mysql中 LogInfo(storeFileinfo, url: {}, fdfs_file_url);// 把文件写入file_infoif (storeFileinfo(db_conn, cache_conn, user, file_name, file_md5,long_file_size, fileid, fdfs_file_url) 0) {LogError(storeFileinfo failed );ret -1;// 严谨而言这里需要删除 已经上传的文件goto END;}ret 0;value[code] 0;str_json value.toStyledString(); // json序列化, 直接用writer是紧凑方式这里toStyledString是格式化更可读方式
3.5 上传文件之秒传
上节提到文件上传时会先校验MD5如果匹配则说明服务器已经存在该文件客户端不需要再去调用 upload 接口上传文件。达到秒传效果。本节介绍的就是秒传。 1、sql 语句从文件信息表file_info获取此md5值文件的文件计数器 count表示有多少个用户拥有这个MD5值的文件
sprintf(sql_cmd, select count from file_info where md5 %s, md5);2、若查询不到秒传失败
3、若查询到再查询此用户是否已经有此文件 ◼ 如果存在说明此用户已经保存此文件不能能重复上传 ◼ 如果不存在修改file_info对应MD5文件的count字段进行1表示多一个用户拥有。同时向用户文件列表user_file_list插入一条数据。
insert into user_file_list(user, md5, create_time, file_name, shared_status, pv) values (%s, %s, %s, %s, %d, %d),user, md5, time_str, filename, 0, 0);//秒传处理
void handleDealMd5(const char *user, const char *md5, const char *filename,string str_json) {Md5State md5_state Md5Failed;int ret 0;int file_ref_count 0;char sql_cmd[SQL_MAX_LEN] {0};CDBManager *db_manager CDBManager::getInstance();CDBConn *db_conn db_manager-GetDBConn(tuchuang_slave);AUTO_REL_DBCONN(db_manager, db_conn);CacheManager *cache_manager CacheManager::getInstance();CacheConn *cache_conn cache_manager-GetCacheConn(token);AUTO_REL_CACHECONN(cache_manager, cache_conn);// 1、sql 语句获取此md5值文件的文件计数器 countsprintf(sql_cmd, select count from file_info where md5 %s, md5);LogInfo(执行: {}, sql_cmd);//返回值 0成功并保存记录集1没有记录集2有记录集但是没有保存-1失败file_ref_count 0;ret GetResultOneCount(db_conn, sql_cmd, file_ref_count); //执行sql语句LogInfo(ret: {}, file_ref_count: {}, ret, file_ref_count);if (ret 0) //2、有结果, 并且返回 file_info被引用的计数 file_ref_count{//2.1 查看此用户是否已经有此文件如果存在说明此文件已上传无需再上传sprintf(sql_cmd,select * from user_file_list where user %s and md5 %s and file_name %s,user, md5, filename);LogInfo(执行: {}, sql_cmd);//返回值 1: 表示已经存储了有这个文件记录ret CheckwhetherHaveRecord(db_conn, sql_cmd); // 检测个人是否有记录if (ret 1) //如果有结果说明此用户已经保存此文件{LogWarn(user: {}- filename: {}, md5: {}已存在, user, filename, md5);md5_state Md5FileExit; // 此用户已经有该文件了不能重复上传goto END;}// 2.2 此用户没有此文件修改file_info中的count字段1 count文件引用计数表示多了一个用户拥有该文件sprintf(sql_cmd, update file_info set count %d where md5 %s,file_ref_count 1, md5);LogInfo(执行: {}, sql_cmd);if (!db_conn-ExecutePassQuery(sql_cmd)) {LogError({} 操作失败, sql_cmd);md5_state Md5Failed; // 更新文件引用计数失败这里也认为秒传失败宁愿他再次上传文件goto END;}// 2.3 同时向user_file_list用户文件列表插入一条数据//当前时间戳struct timeval tv;struct tm *ptm;char time_str[128];//使用函数gettimeofday()函数来得到时间。它的精度可以达到微妙gettimeofday(tv, NULL);ptm localtime(tv.tv_sec); //把从1970-1-1零点零分到当前时间系统所偏移的秒数时间转换为本地时间// strftime()// 函数根据区域设置格式化本地时间/日期函数的功能将时间格式化或者说格式化一个时间字符串strftime(time_str, sizeof(time_str), %Y-%m-%d %H:%M:%S, ptm);// 用户列表增加一个文件记录sprintf(sql_cmd,insert into user_file_list(user, md5, create_time, file_name, shared_status, pv) values (%s, %s, %s, %s, %d, %d),user, md5, time_str, filename, 0, 0);LogInfo(执行: {}, sql_cmd);if (!db_conn-ExecuteCreate(sql_cmd)) {LogError({} 操作失败, sql_cmd);md5_state Md5Failed;// 恢复引用计数sprintf(sql_cmd, update file_info set count %d where md5 %s,file_ref_count, md5);LogInfo(执行: {}, sql_cmd);if (!db_conn-ExecutePassQuery(sql_cmd)) {LogError({} 操作失败, sql_cmd);}goto END;}//查询用户文件数量, 用户数量1if (CacheIncrCount(cache_conn, FILE_USER_COUNT string(user)) 0) {LogWarn(CacheIncrCount failed); // 这个可以在login的时候从mysql加载}md5_state Md5Ok;} else //3、没有结果秒传失败{LogInfo(秒传失败);md5_state Md5Failed;goto END;}END:/*秒传文件秒传成功 {code: 0}秒传失败 {code:1}文件已存在{code: 5}*/int code (int)md5_state;encodeMd5Json(code, str_json);
}3.6 获取共享文件列表或下载榜
分 3 个接口 ◼ 获取共享文件个数 /api/sharefiles?cmdcount ◼ 获取共享文件列表 /api/sharefiles?cmdnormal ◼ 获取共享文件下载排行榜 /api/sharefiles?cmdpvdesc
1、共享文件个数 /api/sharefiles?cmdcount 获取共享文件数量我们是先查redis若有直接返回即可。若没有再查MySQL并且把数据同步到redis。
int getShareFilesCount(CDBConn *db_conn, CacheConn *cache_conn, int count) {int ret 0;int64_t file_count 0;// 先查看用户是否存在string str_sql;// 1. 先从redis里面获取if (CacheGetCount(cache_conn, FILE_PUBLIC_COUNT, file_count) 0) {LogWarn(CacheGetCount FILE_PUBLIC_COUNT failed);ret -1;}// 2. 若数量为0从mysql查询确定是否为0if (file_count 0) {// 2.1 从mysql加载if (DBGetShareFilesCount(db_conn, count) 0) {LogError(DBGetShareFilesCount failed);return -1;}file_count (int64_t)count;// 2.2 同步数据到redisif (CacheSetCount(cache_conn, FILE_PUBLIC_COUNT, file_count) 0) // 失败了那下次继续从mysql加载{LogError(CacheSetCount FILE_PUBLIC_COUNT failed);return -1;}ret 0;}// 3. 若数量不为0直接返回count file_count;return ret;
}2、 获取共享文件列表 /api/sharefiles?cmdnormal 核心就是执行sql语句然后把返回的数据解析成json打包。 str_sql FormatString(select share_file_list.*, file_info.url, file_info.size, file_info.type from file_info, \share_file_list where file_info.md5 share_file_list.md5 limit %d, %d,start, count);LogInfo(执行: {}, str_sql);result_set db_conn-ExecuteQuery(str_sql.c_str());if (result_set) {// 遍历所有的内容// 获取大小file_count 0;while (result_set-Next()) {Json::Value file;file[user] result_set-GetString(user);file[md5] result_set-GetString(md5);file[file_name] result_set-GetString(file_name);file[share_status] result_set-GetInt(share_status);file[pv] result_set-GetInt(pv);file[create_time] result_set-GetString(create_time);file[url] result_set-GetString(url);file[size] result_set-GetInt(size);file[type] result_set-GetString(type);files[file_count] file;file_count;}if (file_count 0)root[files] files;ret 0;delete result_set;} else {ret -1;}3、获取共享文件下载排行榜 /api/sharefiles?cmdpvdesc 排行榜的逻辑比较简单就是使用 redis 的 ZSET 做排行榜。 这里涉及到 mysql 和 redis获取返回的是文件名和下载量。这里文件名可能重名所以这里用了文件 md5文件名作为唯一 ID。
1先从 ZSET 获取排行榜此时的 member 是 md5文件名score 是下载量 pv 2然后将 member 的 md5文件名 通过 HASH 查找对应的文件名 filename 3将文件名 filename 和下载量 pv 返回给前端展示。 4下载文件后需要更新排行榜。 具体步骤 a) mysql共享文件数量和redis共享文件数量对比判断是否相等 b) 如果不相等清空redis数据从mysql中导入数据到redis (mysql和redis交互) //3、mysql共享文件数量和redis共享文件数量对比判断是否相等if (redis_num ! sql_num) { //4、如果不相等清空redis数据重新从mysql中导入数据到redis//(mysql和redis交互)// a) 清空redis有序数据cache_conn-Del(FILE_PUBLIC_ZSET); // 删除集合cache_conn-Del(FILE_NAME_HASH); // 删除hash 理解 这里hash和集合的关系// b) 从mysql中导入数据到redis// sql语句strcpy( sql_cmd, select md5, file_name, pv from share_file_list order by pv desc);LogInfo(执行: {}, sql_cmd);pCResultSet db_conn-ExecuteQuery(sql_cmd);if (!pCResultSet) {LogError({} 操作失败, sql_cmd);ret -1;goto END;}// mysql_fetch_row从使用mysql_store_result得到的结果结构中提取一行并把它放到一个行结构中。// 当数据用完或发生错误时返回NULL.while (pCResultSet-Next()) {char field[1024] {0};string md5 pCResultSet-GetString(md5); // 文件的MD5string file_name pCResultSet-GetString(file_name); // 文件名int pv pCResultSet-GetInt(pv);sprintf(field, %s%s, md5.c_str(),file_name.c_str()); //文件标示md5文件名//增加有序集合成员cache_conn-ZsetAdd(FILE_PUBLIC_ZSET, pv, field);//增加hash记录cache_conn-Hset(FILE_NAME_HASH, field, file_name);}}c) 从redis读取数据给前端反馈相应信息
3.7 分享/ 删除文件/ 更新下载数
1、/api/dealfile?cmdshare 分享文件 具体流程是 ◼ 先判断此文件是否已经分享判断集合有没有这个文件如果有说明别人已经分享此文件中断操作(redis操作)。 ◼ 如果集合没有此元素可能因为redis中没有记录再从mysql中查询如果mysql也没有说明真没有(mysql操作) ◼ 如果mysql有记录而redis没有记录说明redis没有保存此文件redis保存此文件信息后再中断操作(redis操作) ◼ 如果此文件没有被分享mysql保存一份持久化操作(mysql操作) ◼ redis集合中增加一个元素(redis操作) ◼ redis对应的hash也需要变化 (redis操作) //文件标示md5文件名sprintf(fileid, %s%s, md5.c_str(), filename.c_str());if (cache_conn) {ret2 cache_conn-ZsetExit(FILE_PUBLIC_ZSET, fileid);} else {ret2 0;}LogInfo(fileid: {}, ZsetExit: {}, fileid, ret2);//1、先判断此文件是否已经分享判断集合有没有这个文件如果有说明别人已经分享此文件中断操作(redis操作)if (ret2 1) //存在{LogWarn(别人已经分享此文件);share_state ShareHad;goto END;} else if (ret2 0) //不存在{//2、如果集合没有此元素可能因为redis中没有记录再从mysql中查询如果mysql也没有说明真没有(mysql操作)//3、如果mysql有记录而redis没有记录说明redis没有保存此文件redis保存此文件信息后再中断操作(redis操作)//查看此文件别人是否已经分享了sprintf(sql_cmd,select * from share_file_list where md5 %s and file_name %s,md5.c_str(), filename.c_str());//返回值1有记录ret2 CheckwhetherHaveRecord(db_conn,sql_cmd); //执行sql语句, 最后一个参数为NULL//,如果有则说明没有及时保持到redis这里需要保存到redisif (ret2 1) //说明有结果别人已经分享此文件{// redis保存此文件信息cache_conn-ZsetAdd(FILE_PUBLIC_ZSET, 0, fileid);cache_conn-Hset(FILE_NAME_HASH, fileid, filename);LogWarn(别人已经分享此文件);share_state ShareHad;goto END;}} else //出错{ret -1;goto END;}//4、如果此文件没有被分享mysql保存一份持久化操作(mysql操作)// sql语句, 更新共享标志字段sprintf(sql_cmd,update user_file_list set shared_status 1 where user %s and md5 %s and file_name %s,user.c_str(), md5.c_str(), filename.c_str());if (!db_conn-ExecuteUpdate(sql_cmd, false)) {LogError({} 操作失败, sql_cmd);ret -1;goto END;}time_t now;;char create_time[TIME_STRING_LEN];//获取当前时间now time(NULL);strftime(create_time, TIME_STRING_LEN - 1, %Y-%m-%d %H:%M:%S,localtime(now));//分享文件的信息额外保存在share_file_list保存列表/*-- user 文件所属用户-- md5 文件md5-- create_time 文件共享时间-- file_name 文件名字-- pv 文件下载量默认值为1下载一次加1*/sprintf(sql_cmd,insert into share_file_list (user, md5, create_time, file_name, pv) values (%s, %s, %s, %s, %d),user.c_str(), md5.c_str(), create_time, filename.c_str(), 0);if (!db_conn-ExecuteCreate(sql_cmd)) {LogError({} 操作失败, sql_cmd);ret -1;goto END;}// 共享文件数量1ret CacheIncrCount(cache_conn, FILE_PUBLIC_COUNT);if (ret 0) {LogError(CacheIncrCount failed);ret -1;goto END;}//5、redis集合中增加一个元素(redis操作)cache_conn-ZsetAdd(FILE_PUBLIC_ZSET, 0,fileid); // 如果失败是需要撤销mysql数据库的操作的//6、redis对应的hash也需要变化 (redis操作)// fileid ------ filenameLogInfo(Hset FILE_NAME_HASH {}-{}, fileid, filename);ret cache_conn-Hset(FILE_NAME_HASH, fileid, filename);if (ret 0) {LogWarn(Hset FILE_NAME_HASH failed);}share_state ShareOk;
END:return (int)share_state;2、/api/dealfile?cmddel 删除文件 1先判断此文件是否已经分享 ◼ 判断集合有没有这个文件如果有说明别人已经分享此文件(redis 操作) ◼ 如果集合没有此元素可能因为 redis 中没有记录再从 mysql 中查询如果 mysql 也没有说明真没有(mysql 操作) //文件标识文件md5文件名sprintf(fileid, %s%s, md5.c_str(), filename.c_str());//1、先判断此文件是否已经分享判断集合有没有这个文件如果有说明别人已经分享此文件ret2 cache_conn-ZsetExit(FILE_PUBLIC_ZSET, fileid);LogInfo(ret2: {}, ret2);if (ret2 1) //存在{is_shared 1; //共享标志redis_has_record 1; // redis有记录} else if (ret2 0) //不存在{ //2、如果集合没有此元素可能因为redis中没有记录再从mysql中查询如果mysql也没有说明真没有(mysql操作)is_shared 0;// sql语句//查看该文件是否已经分享了sprintf(sql_cmd,select shared_status from user_file_list where user %s and md5 %s and file_name %s,user.c_str(), md5.c_str(), filename.c_str());LogInfo(执行: {}, sql_cmd);int shared_status 0;ret2 GetResultOneStatus(db_conn, sql_cmd, shared_status); //执行sql语句if (ret2 0) {LogInfo(GetResultOneCount share {}, shared_status);is_shared shar ed_status; // 要从mysql里面获取赋值}} else //出错{ret -1;goto END;}2若此文件被分享删除分享列表(share_file_list)的数据 ◼ 如果 mysql 有记录而 redis 没有记录那么分享文件处理只需要处理 mysql (mysql 操作) ◼ 如果 redis 有记录mysql 和 redis 都需要处理删除相关记录 //说明此文件被分享删除分享列表(share_file_list)的数据if (is_shared 1) {//3、如果mysql有记录删除相关分享记录 (mysql操作)// 删除在共享列表的数据, 如果自己分享了这个文件那同时从分享列表删除掉sprintf(sql_cmd,delete from share_file_list where user %s and md5 %s and file_name %s,user.c_str(), md5.c_str(), filename.c_str());LogInfo(执行: {}, sql_cmd);if (!db_conn-ExecuteDrop(sql_cmd)) {LogError({} 操作失败, sql_cmd);ret -1;goto END;}//共享文件的数量-1//查询共享文件数量if (CacheDecrCount(cache_conn, FILE_PUBLIC_COUNT) 0) {LogError(CacheDecrCount 操作失败);ret -1;goto END;}//4、如果redis有记录redis需要处理删除相关记录if (1 redis_has_record) {//有序集合删除指定成员cache_conn-ZsetZrem(FILE_PUBLIC_ZSET, fileid);//从hash移除相应记录cache_conn-Hdel(FILE_NAME_HASH, fileid);}}3删除用户文件列表的数据并使用户文件数量-1 //用户文件数量-1if (CacheDecrCount(cache_conn, FILE_USER_COUNT user) 0) {LogError(CacheDecrCount 操作失败);ret -1;goto END;}//删除用户文件列表数据sprintf(sql_cmd,delete from user_file_list where user %s and md5 %s and file_name %s,user.c_str(), md5.c_str(), filename.c_str());LogInfo(执行: {}, sql_cmd);if (!db_conn-ExecuteDrop(sql_cmd)) {LogError({} 操作失败, sql_cmd);ret -1;goto END;}4文件信息表(file_info)的文件引用计数count减去1。如果count0说明没有用户引用此文件需要在storage删除此文件 //查看该文件文件引用计数sprintf(sql_cmd, select count from file_info where md5 %s,md5.c_str());LogInfo(执行: {}, sql_cmd);count 0;ret2 GetResultOneCount(db_conn, sql_cmd, count); //执行sql语句LogInfo(ret2: {}, count: {}, ret2, count);if (ret2 ! 0) {LogError({} 操作失败, sql_cmd);ret -1;goto END;}if (count 0) {count - 1;sprintf(sql_cmd, update file_info set count%d where md5 %s,count, md5.c_str());LogInfo(执行: {}, sql_cmd);if (!db_conn-ExecuteUpdate(sql_cmd)) {LogError({} 操作失败, sql_cmd);ret -1;goto END;}}if (count 0) //说明没有用户引用此文件需要在storage删除此文件{//查询文件的idsprintf(sql_cmd, select file_id from file_info where md5 %s,md5.c_str());string fileid;CResultSet *result_set db_conn-ExecuteQuery(sql_cmd);if (result_set-Next()) {fileid result_set-GetString(file_id);}//删除文件信息表中该文件的信息sprintf(sql_cmd, delete from file_info where md5 %s, md5.c_str());if (!db_conn-ExecuteDrop(sql_cmd)) {LogWarn({} 操作失败, sql_cmd);}//从storage服务器删除此文件参数为为文件idret2 RemoveFileFromFastDfs(fileid.c_str());if (ret2 ! 0) {LogInfo(RemoveFileFromFastDfs err: {}, ret2);ret -1;goto END;}}ret 0;3、/api/dealfile?cmdpv 更新文件下载计数 用来更新指定文件的下载量每次成功下载一个文件成功后调用该接口更新对应文件的 pv 值。 // sql语句//查看该文件的pv字段sprintf(sql_cmd,select pv from user_file_list where user %s and md5 %s and file_name %s,user.c_str(), md5.c_str(), filename.c_str());LogInfo(执行: {}, sql_cmd);CResultSet *result_set db_conn-ExecuteQuery(sql_cmd);if (result_set result_set-Next()) {pv result_set-GetInt(pv);} else {LogError({} 操作失败, sql_cmd);ret -1;goto END;}//更新该文件pv字段1sprintf(sql_cmd,update user_file_list set pv %d where user %s and md5 %s and file_name %s,pv 1, user.c_str(), md5.c_str(), filename.c_str());LogInfo(执行: {}, sql_cmd);if (!db_conn-ExecuteUpdate(sql_cmd)) {LogError({} 操作失败, sql_cmd);ret -1;goto END;}3.8 取消分享/ 转存/ 更新下载计数
1、/dealsharefile?cmdcancel 取消分享 //文件标示md5文件名sprintf(fileid, %s%s, md5.c_str(), filename.c_str());// 1、共享标志设置为0sprintf(sql_cmd,update user_file_list set shared_status 0 where user %s and md5 %s and file_name %s,user_name.c_str(), md5.c_str(), filename.c_str());LogInfo(执行: {}, sql_cmd);if (!db_conn-ExecuteUpdate(sql_cmd, false)) {LogError({} 操作失败, sql_cmd);ret -1;goto END;}// 2、共享文件数量-1ret2 CacheDecrCount(cache_conn, FILE_PUBLIC_COUNT);if (ret2 0) {LogError({} 操作失败, sql_cmd);ret -1;goto END;}//3、删除在共享列表的数据sprintf(sql_cmd,delete from share_file_list where user %s and md5 %s and file_name %s,user_name.c_str(), md5.c_str(), filename.c_str());LogInfo(执行: {}, ret {}, sql_cmd, ret);if (!db_conn-ExecuteDrop(sql_cmd)) {LogError({} 操作失败, sql_cmd);ret -1;goto END;}//4、redis记录操作//4.1 有序集合删除指定成员ret cache_conn-ZsetZrem(FILE_PUBLIC_ZSET, fileid);if (ret ! 0) {LogInfo(执行: ZsetZrem 操作失败);goto END;}//4.2 从hash移除相应记录LogInfo(Hdel FILE_NAME_HASH {}, fileid);ret cache_conn-Hdel(FILE_NAME_HASH, fileid);if (ret 0) {LogInfo(执行: hdel 操作失败: ret {}, ret);goto END;}2、/api/dealsharefile?cmde save 转存文件 ◼ 先查询是个人文件列表是否已经存在该文件。 ◼ 增加 file_info表的 count 计数表示多一个人保存了该文件。 ◼ 个人的 user_file_list 增加一条文件记录 ◼ 更新个人的 user_file_count 当我们转存该文件后即使分享者删除自己的文件不会影响到我们自己转存的文件。 //查看此用户文件名和md5是否存在如果存在说明此文件存在sprintf(sql_cmd,select * from user_file_list where user %s and md5 %s and file_name %s,user_name.c_str(), md5.c_str(), filename.c_str());ret2 CheckwhetherHaveRecord(db_conn, sql_cmd); // 有记录返回1错误返回-1无记录返回0if (ret2 1) { //如果有结果说明此用户已有此文件LogError(user_name: {}, filename: {}, md5: {} 已存在, user_name, filename, md5);ret -2; //返回-2错误码goto END;}if (ret2 0) {LogError({} 操作失败, sql_cmd);ret -1; //返回-1错误码goto END;}//文件信息表查找该文件的计数器sprintf(sql_cmd, select count from file_info where md5 %s, md5.c_str());count 0;ret2 GetResultOneCount(db_conn, sql_cmd, count); //执行sql语句if (ret2 ! 0) {LogError({} 操作失败, sql_cmd);ret -1;goto END;}// 1、修改file_info中的count字段1 count 文件引用计数sprintf(sql_cmd, update file_info set count %d where md5 %s,count 1, md5.c_str());if (!db_conn-ExecuteUpdate(sql_cmd)) {LogError({} 操作失败, sql_cmd);ret -1;goto END;}// 2、user_file_list插入一条数据gettimeofday(tv, NULL);ptm localtime(tv.tv_sec); //把从1970-1-1零点零分到当前时间系统所偏移的秒数时间转换为本地时间// strftime()// 函数根据区域设置格式化本地时间/日期函数的功能将时间格式化或者说格式化一个时间字符串strftime(time_str, sizeof(time_str), %Y-%m-%d %H:%M:%S, ptm);// sql语句/*-- 用户文件列表-- user 文件所属用户-- md5 文件md5-- create_time 文件创建时间-- file_name 文件名字-- shared_status 共享状态, 0为没有共享 1为共享-- pv 文件下载量默认值为0下载一次加1*/sprintf(sql_cmd,insert into user_file_list(user, md5, create_time, file_name, shared_status, pv) values (%s, %s, %s, %s, %d, %d),user_name.c_str(), md5.c_str(), time_str, filename.c_str(), 0, 0);if (!db_conn-ExecuteCreate(sql_cmd)) {LogError({} 操作失败, sql_cmd);ret -1;goto END;}// 3、查询用户文件数量更新该字段数量1if (CacheIncrCount(cache_conn, FILE_USER_COUNT user_name) 0) {LogError(CacheIncrCount 操作失败);ret -1;goto END;}3、/api/dealsharefile?cmdpv 更新共享文件下载计数 更新 share_file_list 的 pv 值 更新 redis 里的 FILE_PUBLIC_ZSET用作排行榜 //文件标示md5文件名sprintf(fileid, %s%s, md5.c_str(), filename.c_str());//1、mysql的下载量1(mysql操作)// sql语句//查看该共享文件的pv字段sprintf(sql_cmd,select pv from share_file_list where md5 %s and file_name %s,md5.c_str(), filename.c_str());LogInfo(执行: {}, sql_cmd);CResultSet *result_set db_conn-ExecuteQuery(sql_cmd);if (result_set result_set-Next()) {pv result_set-GetInt(pv);} else {LogError({} 操作失败, sql_cmd);ret -1;goto END;}//更新该文件pv字段1sprintf(sql_cmd,update share_file_list set pv %d where md5 %s and file_name %s,pv 1, md5.c_str(), filename.c_str());LogInfo(执行: {}, sql_cmd);if (!db_conn-ExecuteUpdate(sql_cmd, false)) {LogError({} 操作失败, sql_cmd);ret -1;goto END;}//2、判断元素是否在集合中(redis操作)ret2 cache_conn-ZsetExit(FILE_PUBLIC_ZSET, fileid);if (ret2 1) //3、如果存在有序集合score1{ ret cache_conn-ZsetIncr(FILE_PUBLIC_ZSET,fileid); // zrange FILE_PUBLIC_ZSET 0 -1 withscores 查看if (ret ! 0) {LogError(ZsetIncr 操作失败);}} else if (ret2 0) //4、如果不存在从mysql导入数据{ //5、redis集合中增加一个元素(redis操作)cache_conn-ZsetAdd(FILE_PUBLIC_ZSET, pv 1, fileid);//6、redis对应的hash也需要变化 (redis操作)// fileid ------ filenamecache_conn-Hset(FILE_NAME_HASH, fileid, filename);} else //出错{ret -1;goto END;}3.9 图床分享图片
1、/api/sharepic?cmdshare 请求图片分享 前端 1访问链接http://xxx.xxx.xxx.xxx/602fdf30db2aacf517badf456512123该 访问链接由 web 服务器提供。 2访问链接的 web 向 后台服务器请求图片下载地址 请求接口 http://xxx.xxx.xxx.xxx/api/sharepic?cmdbrowse 请求格式 { “urlmd5”: “602fdf30db2aacf517badf4565121234” // 来自请求链接 } 返回格式 { “code”: 0, “url”: “http://xxx.xxx.xxx.xxx/602fdf30db2aacf517badf4565121234”, // 图片的下载地址 “user”: “qingfu”, // 分享者用户名 “pv”: 1, // 浏览次数 “time”: “2021-12-12 11:23:0” // 分享时间 } web 页面获取到 url 后下载图片显示并显示分享者用户名和分享时间。 // 1. 生成urlmd5string urlmd5;urlmd5 RandomString(32); // 这里我们先简单的直接使用随机数代替 MD5的使用LogInfo(urlmd5: {}, urlmd5);// 2. 插入share_picture_list即添加图片分享记录time_t now;//获取当前时间now time(NULL);strftime(create_time, TIME_STRING_LEN - 1, %Y-%m-%d %H:%M:%S,localtime(now));sprintf(sql_cmd,insert into share_picture_list (user, filemd5, file_name, urlmd5, key, pv, create_time) values (%s, %s, %s, %s, %s, %d, %s),user, filemd5, file_name, urlmd5.c_str(), key, 0, create_time);LogInfo(执行: {}, sql_cmd);if (!db_conn-ExecuteCreate(sql_cmd)) {LogError({} 操作失败, sql_cmd);ret -1;goto END;}//3、文件信息表获取该共享图片的数量sprintf(sql_cmd, select count from file_info where md5 %s, filemd5);count 0;ret GetResultOneCount(db_conn, sql_cmd, count); //执行sql语句if (ret ! 0) {LogError({} 操作失败, sql_cmd);ret -1;goto END;}// 4、修改file_info中的count字段即该共享图片的数量1 count 文件引用计数sprintf(sql_cmd, update file_info set count %d where md5 %s,count 1, filemd5);if (!db_conn-ExecuteUpdate(sql_cmd)) {LogError({} 操作失败, sql_cmd);ret -1;goto END;}// 5、 增加分享图片计数 SHARE_PIC_COUNTdarrenif (CacheIncrCount(cache_conn, SHARE_PIC_COUNT string(user)) 0) {LogError( CacheIncrCount 操作失败);}2、/api/sharepic?cmdbrowse 请求浏览图片 请求接口 http://xxx.xxx.xxx.xxx/api/sharepic?cmdbrowse主要用来返回具体的图片下载地址。 // 1. 先从分享图片列表查询到文件信息sprintf(sql_cmd,select user, filemd5, file_name, pv, create_time from share_picture_list where urlmd5 %s,urlmd5);LogDebug(执行: {}, sql_cmd);result_set db_conn-ExecuteQuery(sql_cmd);if (result_set result_set-Next()) {user result_set-GetString(user);filemd5 result_set-GetString(filemd5);file_name result_set-GetString(file_name);pv result_set-GetInt(pv);create_time result_set-GetString(create_time);delete result_set;} else {if (result_set)delete result_set;ret -1;goto END;}// 2. 通过文件的MD5查找对应的url地址sprintf(sql_cmd, select url from file_info where md5 %s,filemd5.c_str());LogInfo(执行: {}, sql_cmd);result_set db_conn-ExecuteQuery(sql_cmd);if (result_set result_set-Next()) {picture_url result_set-GetString(url);delete result_set;} else {if (result_set)delete result_set;ret -1;goto END;}// 3. 更新浏览次数 可以考虑保存到redis减少数据库查询的压力pv 1; //浏览计数增加sprintf(sql_cmd,update share_picture_list set pv %d where urlmd5 %s, pv,urlmd5);LogDebug(执行: {}, sql_cmd);if (!db_conn-ExecuteUpdate(sql_cmd)) {LogError({} 操作失败, sql_cmd);ret -1;goto END;}3、/api/sharepic?cmdnormal 我的图片分享
4、/api/sharepic?cmdcance 取消图片分享 需要注意的是如果文件引用次数分享次数为0说明没人引用了需要从文件信息表和fastdfs的storage删除。 // 获取文件md5LogInfo(urlmd5: {}, urlmd5);// 1. 查看是否有分享记录先从分享图片列表查询到文件信息sprintf(sql_cmd, select filemd5 from share_picture_list where urlmd5 %s, urlmd5);LogDebug(执行: {}, sql_cmd);result_set db_conn-ExecuteQuery(sql_cmd);if (result_set result_set-Next()) {filemd5 result_set-GetString(filemd5);delete result_set;} else {if (result_set)delete result_set;ret -1;goto END;}//2、查询文件信息表(file_info)的文件引用计数countsprintf(sql_cmd,select count, file_id from file_info where md5 %s for update,filemd5.c_str()); //LogInfo(执行: {}, sql_cmd);result_set db_conn-ExecuteQuery(sql_cmd);if (result_set result_set-Next()) {fileid result_set-GetString(file_id);count result_set-GetInt(count);delete result_set;} else {if (result_set)delete result_set;LogError({} 操作失败, sql_cmd);ret -1;// db_conn-Rollback();goto END;}// 3. 更新文件信息表的文件引用数count - 1if (count 0) {count - 1;sprintf(sql_cmd, update file_info set count%d where md5 %s,count, filemd5.c_str());LogInfo(执行: {}, sql_cmd);if (!db_conn-ExecuteUpdate(sql_cmd)) {LogError({} 操作失败, sql_cmd);ret -1;// db_conn-Rollback();goto END;}}//4、删除在共享图片列表的数据sprintf(sql_cmd,delete from share_picture_list where user %s and urlmd5 %s,user, urlmd5);LogInfo(执行: {}, sql_cmd);if (!db_conn-ExecutePassQuery(sql_cmd)) {LogError({} 操作失败, sql_cmd);ret -1;// db_conn-Rollback();goto END;}// 5、若count0说明没有用户引用此文件需要在文件信息表和storage删除此文件if (count 0){//删除文件信息表中该文件的信息sprintf(sql_cmd, delete from file_info where md5 %s,filemd5.c_str());LogInfo(执行: {}, sql_cmd);if (!db_conn-ExecuteDrop(sql_cmd)) {LogWarn({} 操作失败, sql_cmd);ret -1;goto END;}LogWarn(RemoveFileFromFastDfs);//从storage服务器删除此文件参数为为文件idret2 RemoveFileFromFastDfs(fileid.c_str());if (ret2 ! 0) {LogError(RemoveFileFromFastDfs err: {}, ret2);ret -1;goto END;}}// 6、共享图片数量-1if (CacheDecrCount(cache_conn, SHARE_PIC_COUNT string(user)) 0) {LogError(CacheDecrCount failed); // 即使失败 也可以下次从mysql加载计数}本专栏知识点是通过零声教育的系统学习进行梳理总结写下文章对c/clinux课程感兴趣的读者可以点击链接,详细查看详细的服务器课程