前言
新版本OA系统已于今日开发完毕,碰巧写前端的同事去抽掉走写别的项目了,所以前后端开发的任务就交到了我的身上,虽然前端不是很擅长,但是CV改一改还是能跑起来的,正好借这个机会写一下OA系统中的审批功能吧。
这就是审批模块的主页,因为审批是有层级的,所以在状态栏里是有一个临时状态的,第一个状态就属于临时状态,这个状态针对于当前登录人(审批人)的审批状态,后面的状态是针对这条数据的审批状态。打个比方说,一条报销审批,先到部门经理哪里,然后在部门经理的页面就显示待审批,等部门经理审批完后,临时状态就变成了审批完成,单后面的状态还是“未完成审批”,只有当这条数据彻底完成后,才会显示完成审批。在审批备注里就变成了部门经理已审批完毕,等待财务审批。这时在财务人员的OA页面中显示的是这条报销数据的临时状态是未审批,状态是“未完成审批”。
前端
<template>
<div>
<div slot="top">
<el-form>
<el-row :gutter="5">
<el-col :span="3">
<el-form-item>
<el-select v-model="searchParameter.dome3" clearable placeholder="合同类型" style="width: 100%">
<el-option
v-for="item in bxTypeOptions"
:key="item.index"
:label="item.value"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item>
<el-input placeholder="合同编号" v-model="searchParameter.htorder" clearable></el-input>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item>
<el-input placeholder="甲方名称" v-model="searchParameter.a" clearable></el-input>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item>
<el-input placeholder="乙方名称" v-model="searchParameter.b" clearable></el-input>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item>
<el-input placeholder="经办人员" v-model="searchParameter.username" clearable></el-input>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item>
<el-select v-model="searchParameter.status" clearable placeholder="审批状态" style="width: 100%">
<el-option
v-for="item in spTypeOptions"
:key="item.index"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item>
<el-select v-model="searchParameter.status4" clearable placeholder="审批完成" style="width: 100%">
<el-option
v-for="item in spExceTypeOptions"
:key="item.index"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="3">
<el-button-group>
<el-tooltip content="查询" placement="bottom" open-delay="1000" effect="light">
<el-button type="primary" icon="el-icon-search" @click="Search()"></el-button>
</el-tooltip>
<el-tooltip content="重置" placement="bottom" open-delay="1000" effect="light">
<el-button type="primary" icon="el-icon-refresh" @click="ResetParameter()"></el-button>
</el-tooltip>
<el-tooltip content="导出" placement="bottom" open-delay="1000" effect="light">
<el-button type="primary" icon="el-icon-upload" @click="exportExcel()"></el-button>
</el-tooltip>
<el-tooltip content="高级搜索" placement="bottom" open-delay="1000" effect="light">
<el-button type="primary" icon="el-icon-zoom-in"></el-button>
</el-tooltip>
</el-button-group>
</el-col>
</el-row>
</el-form>
</div>
<el-table
id="table" :data="tableData" :showPage="false" ref="table" style="width: 100%" :border="true"
:header-cell-style="{
background: '#FFFAF0',
color: '#000',fontFamily:'SimHei',fontSize:'16px'}"
>
<el-table-column
type="selection"
width="55px">
</el-table-column>
<el-table-column
label="合同类型"
prop="dome3" min-width="8%">
</el-table-column>
<el-table-column
label="合同编号"
prop="htorder" min-width="10%">
</el-table-column>
<el-table-column
label="甲方名称"
prop="a" min-width="8%">
</el-table-column>
<el-table-column
label="乙方名称"
prop="b" min-width="8%">
</el-table-column>
<el-table-column label="内容简介" prop="sqwhy" min-width="8%"/>
<el-table-column label="经办人" prop="username" min-width="8%"/>
<el-table-column label="签订时间" prop="qddate" min-width="10%" show-overflow-tooltip>
</el-table-column>
<el-table-column label="状态" prop="dome1" min-width="12%" align="center">
<template slot-scope="scope">
<el-tag size="mini" :hit='true' :type="scope.row.spstatuls === '1' ? '': 'warning'">
{{scope.row.spstatuls === '1' ? '已审批' : '待审批'}}
</el-tag>
<el-tag size="mini" :hit='true' :type="scope.row.dome1 === '4' ? '': 'warning'">
{{scope.row.dome1 === '4' ? '完成审批' : '未完成审批'}}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="spdate" label="审批备注" min-width="20%" show-overflow-tooltip>
</el-table-column>
<el-table-column fixed="right" label="操作" min-width="8%">
<template slot-scope="scope">
<el-button type="text" icon="el-icon-chat-dot-square" @click="ShowDetailDialog(scope.row)">
详细情况
</el-button>
</template>
</el-table-column>
</el-table>
<div class="block">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="1"
:page-sizes="[20, 50, 100, 200, 1000]"
:page-size="50"
layout="total, sizes, prev, pager, next, jumper"
:total="rows">
>
</el-pagination>
</div>
<el-dialog title="详细情况 " width="800px" :visible.sync="DetailDialogValue">
<div>
<el-table
:data="bxItemDetails"
:span-method="objectSpanMethod"
border
:header-cell-style="{fontFamily:'SimHei',fontSize:'14px'}"
>
<el-table-column prop="username" label="姓名" width="180">
<template slot-scope="scope">
<div :style="{fontSize:'18px'}">
{{scope.row.username}}
</div>
</template>
</el-table-column>
<el-table-column prop="details" label="合同详情">
<template slot-scope="scope">
<div :style="{color:scope.row.important == 1 ?'red':'#333',fontSize:scope.row.important == 1 ?'18px':'14px'}">
{{scope.row.details}}
</div>
</template>
</el-table-column>
</el-table>
</div>
<div>
<div :style="{margin:'10px'}">
<span :style="{fontFamily:'SimHei',fontSize:'18px'}">审批流程</span>
</div>
<div :style="{margin:'0 30px'}">
<el-timeline>
<el-timeline-item
v-for="(activity, index) in activities"
:key="index"
:icon="activity.icon"
:type="activity.type"
:color="activity.color"
:size="activity.size"
:timestamp="activity.timestamp">
{{activity.content}}
</el-timeline-item>
</el-timeline>
</div>
</div>
<!-- <div>
<div :style="{margin:'10px'}">
<span :style="{fontFamily:'SimHei',fontSize:'18px'}">报销单图片</span>
</div>
<div style="display:flex" class="border">
<div class="block" v-for="url in images" :key="url">
<div>
<el-image
style="width: 100px; height: 100px; margin:10px"
:src="url"
:fit="contain"
:preview-src-list="images"></el-image>
<el-divider direction="vertical" v-if="images.indexOf(url) != images.length - 1"></el-divider>
</div>
</div>
<div class="block" v-for="url in images" :key="url">
</div>
</div>
</div> -->
<div v-if="invoicePDFShow" >
<div :style="{margin:'10px'}">
<span :style="{fontFamily:'SimHei',fontSize:'18px'}">申请附件</span>
</div>
<div class="border">
<el-table :data="FDPList">
<el-table-column property="filename" label="文件名称" width="200px"></el-table-column>
<el-table-column property="filePath" label="文件路径" show-overflow-tooltip>
</el-table-column>
<el-table-column fixed="right" label="操作" width="150px">
<template slot-scope="scope">
<el-button type="text" icon="el-icon-search" @click="PreviewLoadFile(scope.row.filePath)">
预览
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
<div :style="{margin:'15px'}" v-if="SPShow">
<el-button type="danger" @click="BatchPass('overrule')">驳回</el-button>
<el-button type="success" v-if="!overruleShow" @click="BatchPass('pass')">通过</el-button>
</div>
<div>
<transition name="el-zoom-in-top">
<div v-show="overruleShow">
<el-input
placeholder="驳回原因,Enter确认"
v-model="BatchPassModel.remark"
@keyup.enter.native='BatchPassEnter()'
clearable>
</el-input>
</div>
</transition>
</div>
</el-dialog>
</div>
</template>
因为前端代码篇幅太长,就不沾出全部代码了。写解释的话我也不知道该怎么说,毕竟前端不是咱的强项,那么咱讲一下后端逻辑实现吧。
后端
在后端的控制器中,完成上述功能,其实只要三个方法,一个是整个页面的显示方法,一个是点击详情后的显示方法,另外一个就是审批和驳回的功能实现。我们先来说一下第一个。讲解写在以下代码注释中。
/// <summary>
/// 查询
/// </summary>
/// <returns></returns>
[HttpGet]
[Route("GetHTSpList")]
public async Task<IActionResult> GetHTSpList(string spusername, int pages, int bars, string status4, string status, string squsername, string htorder, string sqorder,string dome3,string a, string b,string username,string qddate)
{
//传来的参数是条件查询,就是在前端页面上的搜索栏
try
{
String spStatu = "";
//这个数组声明就是审批层级的状态,0是驳回的状态
string[] lvArr = { "0", "1", "2", "4" };
ArrayList al = new ArrayList(lvArr);
//这个针对的是前端分页功能,当前端不选择分页时,默认为一页显示50条数据
if (bars == 0 || bars == null) bars = 50;
if (pages == 0 || pages == null) pages = 1;
//判断前端传来的字段,如果是空的话,返回
if (string.IsNullOrWhiteSpace(spusername)) throw new Exception("无此用户,重新登陆后再试");
var tablename = "log_ht_gd";
List<LOG_HT_GD> logHTList = new List<LOG_HT_GD>();
List<HTCheckViewModel> ykCheckViewModel = new List<HTCheckViewModel>();
//根据名称和表名查询层级表,来查询状态
var vr = await fsql.Select<log_sp_Newstatus>()
.Where(x => x.spusername == spusername && x.tablename == tablename)
.FirstAsync();
//如果没查出来数据的话代表当前登录的人员没有审批权限
if (vr == null)
{
return Ok(new { code = 1, msg = "此用户不在审批权限内" });
}
//当前审批状态
var spstatus = vr.spstatus;
//审批后状态
var spLv2 = vr.sp_lv;
List<LOG_HT_GD> fkList = new List<LOG_HT_GD>();
var fsqlFKList = fsql.Select<LOG_HT_GD>();
//条件查询
if (!string.IsNullOrWhiteSpace(squsername))
{
fsqlFKList = fsqlFKList.Where(x => x.username == squsername);
}
if (!string.IsNullOrWhiteSpace(htorder))
{
fsqlFKList = fsqlFKList.Where(x => x.order1 == htorder);
}
if (!string.IsNullOrWhiteSpace(dome3))
{
fsqlFKList = fsqlFKList.Where(x => x.dome3 == dome3);
}
if (!string.IsNullOrWhiteSpace(qddate))
{
fsqlFKList = fsqlFKList.Where(x => x.qddate == Convert.ToDateTime(qddate));
}
if (!string.IsNullOrWhiteSpace(a))
{
fsqlFKList = fsqlFKList.Where(x => x.a == a);
}
if (!string.IsNullOrWhiteSpace(b))
{
fsqlFKList = fsqlFKList.Where(x => x.b == b);
}
if (!string.IsNullOrWhiteSpace(username))
{
fsqlFKList = fsqlFKList.Where(x => x.username == username);
}
if (!string.IsNullOrWhiteSpace(sqorder))
{
fsqlFKList = fsqlFKList.Where(x => x.sqorder == sqorder);
}
if (!string.IsNullOrWhiteSpace(status4))
{
if (status4 == "Y")
{
fsqlFKList = fsqlFKList.Where(x => x.dome1 == "4");
}
else
{
fsqlFKList = fsqlFKList.Where(x => x.dome1 != "0" && x.dome1 != "4");
}
}
if (status == "Y")
{
for (int i = 0; i < lvArr.ToList().IndexOf(spLv2); i++)
{
al.RemoveAt(0);
}
fkList = await fsqlFKList
.Where(x => al.Contains(x.dome1))
.ToListAsync();
}
else if (status == "N")
{
fkList = await fsqlFKList.Where(x => x.dome1 == spstatus)
.ToListAsync();
}
else if (status == "R")
{
fkList = await fsqlFKList.Where(x => x.dome1 == "0")
.ToListAsync();
}
else
{
fkList = await fsqlFKList.ToListAsync();
}
//条件查询结束
List<HTCheckViewModel> ykCheckViewModel0 = fkList.Adapt<List<HTCheckViewModel>>();
ykCheckViewModel0 = ykCheckViewModel0.GroupBy(x => x.id).Select(a => a.First()).ToList();
ykCheckViewModel0.FindAll(x => al.Contains(x.dome1)).ForEach(x => x.spstatuls = "1");
ykCheckViewModel0.FindAll(x => x.dome1 == spstatus).ForEach(x => x.spstatuls = "0");
//取出审批范围,比如说开发部的经理只能看到并审批开发部的人员报销数据,但是财务可以看到公司所有人员报销信息
var spfw = vr.spfw;
if (spfw == "all") ykCheckViewModel = ykCheckViewModel0;
else
{
//通过前端传的部门负责人编号,获取整个部门的人员姓名
var sq = fsql.Select<Departmenttable, Users_GD>()
.LeftJoin((a, b) => a.bmid == b.demo6)
.Where((a, b) => a.fuzr == spusername)
.ToList((a, b) => b.userName);
//循环部门所有人姓名
for (int j = 0; j < sq.Count; j++)
{
//取出所有人员姓名
var listModel = ykCheckViewModel0.FindAll(x => x.username.Contains(sq[j].ToString()));
for (int i = 0; i < listModel.Count; i++)
{
ykCheckViewModel.Add(listModel[i]);
}
}
}
var rows = ykCheckViewModel.Count();
//分页,根据时间排序
var data = (ykCheckViewModel.OrderByDescending(s => s.qddate).Skip((pages - 1) * bars).Take(bars)).ToList();
return Ok(new { code = 0, data = data, msg = "success", rows = rows });
}
catch (Exception ex)
{
return Ok(new { code = 1, msg = ex.Message });
}
}
这样的话我们就可以根据当前登录人员信息把他部门下所有人员报销数据查出来并返回给前端显示。其次就是点击详情后显示的功能。
/// <summary>
/// Item查询
/// </summary>
/// <returns></returns>
[HttpGet]
[Route("GetHTItem")]
public async Task<IActionResult> GetHTItem(int id)
{
try
{
var fkItem = await fsql.Select<LOG_HT_GD>().Where(x => x.id == id).FirstAsync();
if (fkItem == null)
{
throw new Exception("该申请不存在,刷新后重试");
}
String imagesPath = "Images/" + fkItem.guid + "/";
HTCheckViewModel ykCheckViewModel = fkItem.Adapt<HTCheckViewModel>();
List<fileInfoHT> fileInfoFKList = new List<fileInfoHT>();
List<spLog> spLogList = new List<spLog>();
if (!string.IsNullOrWhiteSpace(fkItem.dome5)) fileInfoFKList.Add(new fileInfoHT() { filename = fkItem.dome5, filePath = imagesPath + fkItem.dome5 });
if (!string.IsNullOrWhiteSpace(fkItem.dome6)) fileInfoFKList.Add(new fileInfoHT() { filename = fkItem.dome5, filePath = imagesPath + fkItem.dome6 });
if (!string.IsNullOrWhiteSpace(fkItem.dome7)) fileInfoFKList.Add(new fileInfoHT() { filename = fkItem.dome5, filePath = imagesPath + fkItem.dome7 });
if (!string.IsNullOrWhiteSpace(fkItem.dome8)) fileInfoFKList.Add(new fileInfoHT() { filename = fkItem.dome5, filePath = imagesPath + fkItem.dome8 });
if (!string.IsNullOrWhiteSpace(fkItem.dome9)) fileInfoFKList.Add(new fileInfoHT() { filename = fkItem.dome5, filePath = imagesPath + fkItem.dome9 });
if (!string.IsNullOrWhiteSpace(fkItem.dome10)) fileInfoFKList.Add(new fileInfoHT() { filename = fkItem.dome5, filePath = imagesPath + fkItem.dome10 });
if (!string.IsNullOrWhiteSpace(fkItem.dome11))
{
spLogList = JsonConvert.DeserializeObject<List<spLog>>(fkItem.dome11);
};
ykCheckViewModel.spLogList = spLogList;
ykCheckViewModel.fileInfoList = fileInfoFKList;
return Ok(new { code = 0, data = ykCheckViewModel, msg = "success" });
}
catch (Exception ex)
{
return Ok(new { code = 1, msg = ex.Message });
}
}
这个东西到是没什么好说的,我们只需要把数据传给前端就好,至于怎么显示审批流程,审批到了哪一步,那是前端应该考虑的事情。前端的事情,和我后端有什么关系呢,哈哈哈嗝。
那么说一下审批和驳回吧,这个写的挺多的。
/// <summary>
/// 批量操作
/// </summary>
/// <param name="jsonData"></param>
/// <returns></returns>
[HttpPost]
[Route("BatchPass")]
public async Task<IActionResult> BatchPass([FromBody] BatchOperate jsonData)
{
try
{
//这个先把前端传来的数据取一下,一会要用得到
var remark = jsonData.remark;
var updtype = jsonData.uptype;
var tablename = jsonData.tablename;
var spusername = jsonData.spusername;
String msg = "审批完成";
//List<spLog> spLogList = new List<spLog>();写进封装方法里
//判断前端点的是审批按钮还是驳回按钮
if (updtype == "pass")
{
List<Batch> glist = jsonData.guidList;//重赋值
//审批后状态
var sp_lv = await fsql.Select<log_sp_Newstatus>()
.Where(x => x.spusername == jsonData.spusername && x.tablename == jsonData.tablename)
.FirstAsync(x => x.sp_lv);
//当前审批人状态
var sp_lvs = await fsql.Select<log_sp_Newstatus>()
.Where(x => x.spusername == jsonData.spusername && x.tablename == jsonData.tablename)
.FirstAsync(x => x.spstatus);
//原来前端是把审批和驳回两个按钮放在操作栏里的,而且在搜索栏里也有批量审批功能,所以写了个循环来进行操作,后来前端取消了这种操作方式,但是懒得改了
for (int i = 0; i < glist.Count; i++)
{
string guid = glist[i].guid;
var bxItem = await fsql.Select<LOG_HT_GD>()
.Where(x => x.guid == guid)
.FirstAsync();
var dome11 = bxItem.dome11;
//判断审批状态,如果是4的话这条数据是被审批完的。这其实也针对的批量操作
if (bxItem.dome1 == "4")
{
msg = "已审批完成,无需再次审批";
return Ok(new { code = 1, msg = msg });
}
这边写了一个封装类,封装的就是以下被注释掉的代码,封装类里又优化了一下。
#region 使用封装类
Encapsulation encapsulation = new Encapsulation(fsql, mapper);
var spLogList = encapsulation.SPList(spusername, dome11, sp_lv, remark,updtype);
#endregion
#region 把此方法封装起来了,可以直接调用封装方法,此方法弃用
当前登录人员姓名
//var userName = await fsql.Select<Users_GD>().Where(x => x.account == spusername).FirstAsync(x => x.userName);
//spLog spLog = new spLog();
//spLog.username = userName;
//spLog.spdate = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
//spLog.spstatu = sp_lv != "4" ? 0 : 1;
//spLog.remark1 = "已通过";
//if (bxItem.dome11 == null || bxItem.dome11 == "")
//{
// spLogList.Add(spLog) ;
//}
//else
//{
// List<spLog> spLogList0 = new List<spLog>();
// spLogList0 = JsonConvert.DeserializeObject<List<spLog>>(bxItem.dome11.ToString());
// spLogList0.Add(spLog);
// spLogList = spLogList0;
//}
#endregion
//审批后的人员编号
var ds = fsql.Select<log_sp_Newstatus>()
.Where(x => x.tablename == jsonData.tablename && x.spstatus == sp_lv)
.ToList(x => x.spusername);
if (ds.Count != 0)
{
var xm = ds[0];
var sqls = fsql.Select<Users_GD>()
.Where(x => x.account == xm)
.ToList(x => x.userName);
//审批后的人员名称
var name = sqls[0];
//审批人的姓名
var sql = await fsql.Select<Users_GD>()
.Where(x => x.account == spusername)
.FirstAsync(x => x.userName);
var sprname = sql;
string spLogListStr = JsonConvert.SerializeObject(spLogList);
//这个地方就是修改了,通过查询当前人的姓名和下一级审批人的姓名,显示到审批备注里
var xg = fsql.Update<LOG_HT_GD>()
.Set(x => x.dome1 == sp_lv)
.Set(x => x.dome11 == spLogListStr)
.Set(x => x.spdate == "【" + sprname + "】已审批,等待【" + name + "】审批")
.Where(x => x.guid == guid)
.ExecuteAffrows();
if (xg <= 0)
{
throw new Exception("审批失败");
}
}
else
{
//老板就是最后一个审批人,不能显示老板已审批完成,等待我审批吧,所以要判断一下,如果是最后一个审批人的话,这个备注状态就改成审批完成+当前时间
string spLogListStr = JsonConvert.SerializeObject(spLogList);
var xgs = fsql.Update<LOG_HT_GD>()
.Set(x => x.dome1 == sp_lv)
.Set(x => x.dome11 == spLogListStr)
.Set(x => x.spdate == "审批完成 【" + DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss") + "】")
.Where(x => x.guid == guid)
.ExecuteAffrows();
}
}
if (glist.Count > 1) msg = "批量审批成功";
return Ok(new { code = 0, msg = msg });
}
else
{
msg = "驳回成功";
List<Batch> glist = jsonData.guidList;//重赋值
//审批后状态
var sp_lv = await fsql.Select<log_sp_Newstatus>()
.Where(x => x.spusername == jsonData.spusername && x.tablename == jsonData.tablename)
.FirstAsync(x => x.sp_lv);
if (string.IsNullOrWhiteSpace(jsonData.remark))
{
throw new Exception("请输入驳回原因");
}
for (int i = 0; i < glist.Count; i++)
{
string guid = glist[i].guid;
var bxItem = await fsql.Select<LOG_HT_GD>()
.Where(x => x.guid == guid)
.FirstAsync();
var dome11 = bxItem.dome11;
if (bxItem.dome1 == "4")
{
msg = "已审批完成,无需再次审批";
return Ok(new { code = 1, msg = msg });
}
#region 使用封装类
Encapsulation encapsulation = new Encapsulation(fsql, mapper);
var spLogList = encapsulation.SPList(spusername, dome11, sp_lv, remark,updtype);
#endregion
#region 此方法已被封装,弃用
//var userName = await fsql.Select<Users_GD>().Where(x => x.account == spusername).FirstAsync(x => x.userName);
//spLog spLog = new spLog();
//spLog.username = userName;
//spLog.spdate = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
//spLog.spstatu = -1;
//spLog.remark1 = jsonData.remark;
//if (bxItem.dome11 == null || bxItem.dome11 == "")
//{
// spLogList.Add(spLog);
//}
//else
//{
// List<spLog> spLogList0 = new List<spLog>();
// spLogList0 = JsonConvert.DeserializeObject<List<spLog>>(bxItem.dome11.ToString());
// spLogList0.Add(spLog);
// spLogList = spLogList0;
//}
#endregion
string spLogListStr = JsonConvert.SerializeObject(spLogList);
var xg = fsql.Update<LOG_HT_GD>()
.Set(x => x.dome1 == "0")
.Set(x => x.dome2 == jsonData.remark)
.Set(x => x.dome11 == spLogListStr)
.Set(x => x.spdate == "已驳回")
.Where(x => x.guid == guid)
.ExecuteAffrows();
if (xg <= 0)
{
throw new Exception("驳回失败!");
}
}
if (glist.Count > 1) msg = "批量驳回成功";
return Ok(new { code = 0, msg = "驳回成功!" });
}
}
catch (Exception ex)
{
return Ok(new { code = 1, msg = ex.Message });
}
}
还是比较简单的,封装类里的代码在上述注释掉的代码中也有展示,因为耦合性太高,所以封装了一下并优化。总体来说,后端的难点并不是太难。