项目后端代码链接:oriAccounts-backend: oriAccounts记账软件后端代码 (gitee.com)
一、项目概述与目标回顾
本项目为与oriAccounts(手机记账app)前端项目对接的后端项目,主要实现产品的端云数据同步功能。
项目主要实现以下几个功能:
-
邀请码购买制度实现云端同步功能。
-
通过邀请码+密码开启功能,以邀请码为uid,进行云端同步。
-
通过版本号进行同步。如果本地提交的版本号大于云端,则返回上传成功状态,并同步本地数据至云端。如果本地提交的版本号小于或等于云端,返回提示,并将云端数据返回。
二、实现方案
2.1 后端程序架构
后端采用Java语言,SpringBoot框架。
使用标准的Controller-Service-Dao三层架构,Controller层处理请求,Service层进行业务逻辑实现,Dao层与本地存储进行交互(如图)。
2.2 邀请卡申请与验证业务实现
@Service
public class InvitationCardServiceImpl implements InvitationCardService {
@Autowired
private InvitationCardDao invitationCardDao;
@Override
public Result genAndGetInvitationCard() throws IOException {
InvitationCard newCard = InvitationCard.makeInvitationCard();
if (invitationCardDao.createNewUserFolder(newCard)) {
return new Result(10001,"成功生成并获取邀请码",newCard);
} else {
return new Result(10000,"生成并获取邀请码失败,请联系管理员",null);
}
}
@Override
public int invitationCardValidation(InvitationCard invitationCard) {
return invitationCardDao.invitationCardValidation(invitationCard);
}
}
@Repository
public class InvitationCardDao {
private static final String dataPath = "."+File.separator+"data"+File.separator;
public boolean createNewUserFolder(InvitationCard i) throws IOException {
if (!new File(dataPath).exists()) {
new File(dataPath).mkdir();
}
File f = new File(dataPath+i.getCard());
File pwdF = new File(dataPath+i.getCard()+File.separator+i.getPwd());
if (f.mkdir()) {
return pwdF.createNewFile();
} else {
return false;
}
}
public int invitationCardValidation(InvitationCard invitationCard) {
if (!new File(dataPath+invitationCard.getCard()).exists()) {
return -1; // 邀请卡号不存在
}
if (!new File(dataPath+invitationCard.getCard()+File.separator+invitationCard.getPwd()).exists()) {
return -2; // 邀请卡密码错误
}
return 0;
}
}
经最初构想,我决定每一个邀请卡作为一个存储单位,邀请卡号作为文件夹名,邀请卡密码作为存储数据的文件名。所以申请邀请卡的业务,可以理解为生成一个邀请卡后,通过该邀请卡创建文件夹和存储数据的文件,而验证邀请卡的业务即可转化为检测文件夹与其中的文件是否存在的业务。经实践验证,该方式可行,且具有方便、快捷的优势。
此外,我还将控制当前是否开放邀请卡的申请的变量存储在了一个单独的文件中,这样可以做到随改随用,不需要再次重启整个后端系统。
2.3 数据同步业务实现
这是整个后端项目的难点所在。想要实现同步功能很简单,但是关键在于怎样实现最简单,怎样实现更符合一般用户习惯,怎样实现能完美的实现端和云两边数据的新旧对比。
首先谈存储格式。因为本项目有accounts和details两个json格式数据需要存储,所以就简单的将他们以明文形式存储在文件中(不要问为什么不考虑用户隐私问题OvO)。另外,引入一个版本号用于端云数据版本的对比(数据存储原型如图)。
然后是代码实现,
@Service
public class DataSyncServiceImpl implements DataSyncService {
@Autowired
private InvitationCardDao invitationCardDao;
@Autowired
private DataSyncDao dataSyncDao;
@Override
public Result dataSync(DataObj dataObj) {
switch (invitationCardDao.invitationCardValidation(dataObj.getInvitationCard())) {
case 0 -> {
if (dataObj.getVersion()>dataSyncDao.getVersion(dataObj.getInvitationCard())) {
if (storeData(dataObj)) {
return new Result(20001, "数据版本大于云端,数据上传成功", null);
} else {
return new Result(20000, "数据上传失败,请联系管理员", null);
}
} else {
return new Result(20002,"数据版本小于或等于云端,数据同步成功",getData(dataObj.getInvitationCard()));
}
}
case -1 -> {
return new Result(20000,"邀请卡不存在",null);
}
case -2 -> {
return new Result(20000,"邀请卡密码不正确",null);
}
}
return null;
}
@Override
public boolean storeData(DataObj dataObj) {
return dataSyncDao.storeData(dataObj);
}
@Override
public DataObj getData(InvitationCard invitationCard) {
return dataSyncDao.getData(invitationCard);
}
}
@Repository
public class DataSyncDao {
public boolean storeData(DataObj dataObj) {
File path = new File("data"+File.separator+dataObj.getInvitationCard().getCard()+File.separator+dataObj.getInvitationCard().getPwd());
try (FileWriter fileWriter = new FileWriter(path)) {
fileWriter.write("version: "+dataObj.getVersion()+"\n");
fileWriter.write("accounts: "+dataObj.getAccounts()+"\n");
fileWriter.write("details: "+dataObj.getDetails()+"\n");
} catch (IOException e) {
return false;
}
return true;
}
public int getVersion(InvitationCard invitationCard) {
File path = new File("data"+File.separator+invitationCard.getCard()+File.separator+invitationCard.getPwd());
try (BufferedReader bufferedReader = new BufferedReader(new FileReader(path))) {
String versionLine = bufferedReader.readLine();
if (versionLine==null) return -1;
return Integer.parseInt(versionLine.substring(versionLine.indexOf(' ')+1));
} catch (IOException e) {
return -1;
}
}
public DataObj getData(InvitationCard invitationCard) {
File path = new File("data"+File.separator+invitationCard.getCard()+File.separator+invitationCard.getPwd());
DataObj d = new DataObj();
d.setInvitationCard(invitationCard);
try (BufferedReader bufferedReader = new BufferedReader(new FileReader(path))) {
String versionLine = bufferedReader.readLine();
d.setVersion(Integer.parseInt(versionLine.substring(versionLine.indexOf(' ')+1)));
String accountsLine = bufferedReader.readLine();
d.setAccounts(accountsLine.substring(accountsLine.indexOf(": ")+2));
String detailsLine = bufferedReader.readLine();
d.setDetails(detailsLine.substring(detailsLine.indexOf(": ")+2));
return d;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
这两段代码比较朴实无华,着重练习了我对于文件以及文件中数据的读写能力。
不过值得一提的是,在获取数据和版本号方法的实现中,我曾想使用正则表达式进行分组匹配,后来通过观察数据特征,找到了更好的办法,即通过截取每一行冒号后的所有内容获取数据(substring方法与indexOf方法配合使用)。
还有一个要点,在指定读写路径时,我均采用了构造一个path变量后,再进行使用的方法。这样能够避免多次输入路径浪费的时间以及可能出现的typo,同时,在path变量中,我使用File.seperator来获取路径分隔符,这样能简单避免不同系统环境下路径分隔符可能不同的问题。
三、项目总结
通过这个项目的开发,我对SpringBoot框架的使用更加熟练,对MVC模式有了更深入的理解。实现业务逻辑时,逐步掌握了简洁高效的代码编写方法。
项目中,我实现了邀请码的生成与验证、本地和云端数据版本比较与同步等功能。其中,使用文件系统存储数据和数据读写方式是这个项目的亮点,通过 Files 和 IO 流的熟练使用读取写入数据,实现了数据的持久化存储。
当然这个项目肯定还不算成熟,如果未来需要迭代这个项目,我会考虑引入数据库,实现用户系统、数据加密、用户权限控制等功能。此外,使用一些云服务如对象存储会更便捷。
总体来说,这个项目锻炼了我独立开发和学习能力,让我对后端开发有了更深的理解。做项目的过程中,我学会了解决实际问题,不断优化代码的方法。这些经验会让我在未来的开发工作中更上一层楼。