# 2 litemall基础系统
目前litemall基础系统主要由litemall数据库、litemall-core模块、litemall-db模块、
litemall-os-api模块和litemall-all模块组成。
实际上,属于litemall**真正的基础系统**是litemall-core模块和litemall-db模块。
litemall-os-api模块只是为了减少开发中对第三方图片存储服务依赖而实现的简单图像存储服务,
建议开发者最终部署时切换到第三方图片存储服务。litemall-all模块只是一个包裹模块,
没有任何代码,其作用是融合三个spring boot模块和litemall-adminm模块静态文件到
一个单独spring boot应用中,并最终打包成war格式的项目安装包。
目前存在的问题:
* `严重`数据库采用git,每次跟新都是5MB数据,影响项目下载速度
* `改善` litemall-db的一些CRUD操作可以基于开源库重构
* `功能`可以参考一些云存储服务的API加强一些功能
## 2.1 litemall.sql
litemall.sql数据库基于nideshop中的[nideshop.sql](https://github.com/tumobi/nideshop/blob/master/nideshop.sql)数据库,然后在实际开发过程中进行了调整和修改:
* 删除了一些目前不必要的表;
* 删除了表中一些目前不必要的字段;
* 行政区域数据litemall_region没有采用原nideshop中的数据,而是采用了[Administrative-divisions-of-China](https://github.com/modood/Administrative-divisions-of-China);
* 表中的某些字段采用JSON;
* 表中的日期或时间字段采用DATE、DATETIME;
* 字段的数据类型粗粒度化,例如避免MEDIUMINT,而是INT;
* 表的数据做了清理、调整和补充(假数据)。
具体不同可以比较litemall-db模块下sql文件夹中nideshop.sql和litemall.sql。
以下讨论一些关键性设计
注意:
> 以下设计基于个人理解,很可能存在不合理或者与实际系统不符合的地方。
### 2.1.1 商品和货品设计
这里商品存在商品,商品属性,商品规格,货品四种表
商品表是一种商品的基本信息,主要包括商品介绍,商品图片,商品所属类目,商品品牌商等;
商品参数表其实也是商品的基本信息,但是由于是一对多关系,因此不能直接保存在商品表中(虽然采用JSON也可以但是不合理),
因此采用独立的商品参数表,通常是商品的一些公共基本商品参数;
商品规格表是商品进一步区分货品的标识,例如同样一款衣服,基本信息一致,基本属性一致,但是在尺寸这个属性上可以
把衣服区分成多个货品,而且造成对应的数量和价格不一致。商品规格可以看着是商品属性,但具有特殊特征。
商品规格和规格值存在以下几种关系:
* 单一规格和单一规格值,最常见的,即当前商品存在一种货品;
* 单一规格和多个规格值,较常见,即当前商品基于某个规格存在多种货品,通常价格都是相同的,当然也可能不相同;
* 多个规格和单一规格值,可以简化成第一种情况,或者采用第四种情况,通常实际情况下不常见;
* 多个规格和多个规格值,通常是两种规格或者三种规格较为常见,而且对应的价格不完全相同。
货品则是最终面向开发者购买的商品标识,存在多个规格值、数量和价格。
因此这里一个商品表项,存在(至少0个)多个商品属性表项目,存在(至少一个)多个商品规格表项,
存在(至少一个)多个货品表项。
举例如下:
* 一个商品“2018春季衣服商品编号1111111”,
* 存在两个商品参数,
* 属性名称“面向人群”,属性值“男士”
* 属性名称“面料”,属性值“100%棉”
* 存在两种规格共八个商品规格项,
* 规格名称“尺寸”,规则值“S”
* 规格名称“尺寸”,规则值“M”
* 规格名称“尺寸”,规则值“L”
* 规格名称“尺寸”,规则值“XL”
* 规格名称“尺寸”,规则值“XXL”
* 规格名称“颜色”,规格值“蓝色”
* 规格名称“颜色”,规格值“灰色”
* 规格名称“颜色”,规格值“黑色”
* 存在15个货品(尺寸*颜色=15个货品)
* 货品“S蓝”,数量 100, 价格 100
* 货品“M蓝”,数量 100, 价格 100
* 货品“L蓝”,数量 100, 价格 100
* 货品“XL蓝”,数量 100, 价格 100
* 货品“XXL蓝”,数量 100, 价格 100
* 货品“S灰”,数量 100, 价格 100
* 货品“M灰”,数量 100, 价格 100
* 货品“L灰”,数量 100, 价格 100
* 货品“XL灰”,数量 100, 价格 100
* 货品“XXL灰”,数量 100, 价格 100
* 货品“S黑”,数量 100, 价格 100
* 货品“M黑”,数量 100, 价格 100
* 货品“L黑”,数量 100, 价格 100
* 货品“XL黑”,数量 0, 价格 100
* 货品“XXL黑”,数量 0, 价格 100
以下是一些细节的讨论:
* 商品表中可能存在数量和价格属性,而货品中也存在数量和价格属性,目前设计这样:
* 商品表的价格应该和某个货品的价格一样,通常应该是所有货品价格的最小值,或者基本款式的价格;
* 商品表中的数量和价格应该仅用于展示,而不能用于最终的订单价格计算;
* 商品表的数量应该设置成所有货品数量的总和;
* 在管理后台添加商品时,如果管理员不填写商品表的数量和价格属性,则自动填写合适的值;如果填写,则使用显示。
* 当小商城中,用户查看商品详情时,初始显示商品表的价格,而如果用户选择具体规格后,则商品
详情里面的价格需要自动切换到该规格的价格。
* 商品规格可以存在规格图片,效果是规格名称前放置规格图片
* 货品也可以存在货品图片,效果是所有规格选定以后对应的货品有货,则在货品价格前放置货品图片
* 如果商品是两种规格,分别是M个和N个规格值,那么通常应该是`M*N`个货品,但是有些货品可能天然不存在。
那么,此时数据库如何来设计,是允许少于`M*N`个项,还是必须等于`M*N`个,而不存在货品的数量设置为0?
*
注意:
> 这里的设计可能与实际项目设计不一致,但是目前是可行的。
> 商品的中文用语“商品”和英语用语“goods”,货品的中文用语“货品”和英语用语“product”可能是不正确的。
### 2.1.2 用户和微信用户设计
目前准备支持用户普通账号登录和微信登录两种方式,两种登录方式仅仅采用一个litemall-user表可能不是很合适。此外,如果进一步支持其他多种第三方登录,那么这里需要重新设计。
### 2.1.3 行政区域设计
原nideship.sql中存在region数据,但是litemall.sql的region数据则来自
[Administrative-divisions-of-China](https://github.com/modood/Administrative-divisions-of-China)项目。从该项目中导入数据到litemall.sql的litemall-province、litemall-city、litemall-area和litemall-street四个表,然后重新生成一个新的litemall-region表。
### 2.1.4 订单设计
订单信息主要由基本信息、商品信息、地址信息、费用信息、快递信息、支付信息和其他信息组成。
* 基本信息
订单创建时的一些基本信息。
* 商品信息
由于订单可以存在多个商品,因此订单的商品信息是由独立的订单商品表记录(可能更应该称为货品)。
* 费用信息
* 快递信息
目前快递信息仅仅记录快递公司、快递单号、快递发出时间、快递接收时间。而如果快递过程中如果存在一些异常,例如物品丢失,则目前系统难以处理。关于快递费的计算,目前采取简单方式,即满88元则免费,否则10元。
* 支付信息
* 其他信息
#### 2.1.4.1 订单状态

订单分成几种基本的状态:
* 已下单
状态码101,此时订单生成,记录订单编号、收货地址信息、订单商品信息和订单相关费用信息;
* 已付款
状态码201,此时用户微信支付付款,系统记录微信支付订单号、支付时间、支付状态;
* 已发货
状态码301,此时商场已经发货,系统记录快递公司、快递单号、快递发送时间。
当快递公司反馈用户签收后,系统记录快递到达时间。
* 已确认
状态码401,当用户收到货以后点击确认收货,系统记录确认时间。
此时,用户可以评价订单商品。
除了这几种正常状态以外,还存在一些非普通的状态:
* 已取消
状态码102,用户下单后未付款之前,点击取消按钮,系统记录结束时间
* 系统自动取消
状态码103,用户下单后半小时未付款则系统自动取消,系统记录结束时间
* 已退款取消
状态码202,用户付款以后未发货前,点击取消按钮,系统进行退款操作,并记录结束信息
* 系统自动确认
状态码402,快递反馈商场用户已签收而用户却不点击确认,超期7天以后,则系统自动确认收货,
用户不能再点击确认收货按钮,但是可以评价订单商品。
当然,以上的基本状态和非普通状态,和实际项目相比仍然相对简单。
此外,当订单状态码是102、103、202、401、402时,订单可以执行删除操作。
虽然并没有最终真正删除,但是用户查看自己订单时将看不到这些“已删除”的订单。
#### 2.1.4.2 状态码所支持的操作
不同的状态码下面,用户能够进行的操作是:
* 101
用户可以“订单支付”、“订单取消”
* 102
用户可以“订单删除”
* 103
用户可以“订单删除”
* 201
用户可以“订单退款取消”
* 202
用户可以“订单删除”
* 301
用户可以“确认收货”
* 401
用户可以“订单删除”、“评价”、“再次购买”
* 402
用户可以“订单删除”、“评价”、“再次购买”
#### 2.1.4.3 售后处理
目前不支持售后或退货相关业务。
#### 2.1.4.4 黑名单
从一些资料看,如果用户订单多次取消,应该加入黑名单。
目前不支持。
### 2.1.5 通用设计
以下是一些表设计中无具体业务意义可通用的字段。
#### 2.1.5.1 deleted
除极少数表,其他所有表都存在`deleted`字段,支持逻辑删除。
因此目前删除数据时,不会直接删除数据,而是修改`deleted`字段。
当然,数据库管理员可以连接到数据库直接删除数据,或者开发者
可以修改这里的逻辑采用物理删除。
#### 2.1.5.2 add_time
除极少数表,其他所有表都存在`add_time`字段,记录数据创建时间。
#### 2.1.5.3 version
如果开发者需要在访问表时采用乐观锁机制,则需要在表中设置`version`字段,
这个字段开发者不需要管理,而是由程序自动使用,来提高乐观锁机制。
具体使用方法可以参考`2.2.8 乐观锁`
## 2.2 litemall-db
litemall-db模块是一个普通的Spring Boot应用,基于mybatis技术提供开发者
访问数据库的功能。
此外,litemall-db最终是作为一个类库被其他模块所依赖使用,因此并不对外
直接服务,没有使用Spring MVC技术。
技术:
* Spring Boot 1.5.10
* MySQL
* Druid
* Mybatis
* PageHelper
* Mybatis Generator
* Mybatis Generator非官方插件mybatis-generator-plugin

这里litemall-db模块可以分成以下几种代码:
* mybatis generator自动化代码
* 业务代码
* 安全代码
* JSON支持代码
* 配置代码
### 2.2.1 自动化代码

如上图所示,双击`mybatis-generator:generate`,则mybatis generator插件会:
1. 读取`mybatis-generator`文件夹下的`generatorConfig.xml`文件
2. 根据`jdbcConnection`访问数据库
3. 根据`table`, 自动生成三种代码:
* src文件夹`org.linlinjava.litemall.db.domain` 包内的Java代码
* src文件夹`org.linlinjava.litemall.db.domain` 包内的Java代码
* resources文件夹`org.linlinjava.litemall.db.domain.dao` 内的XML文件
以上三种代码即可封装对数据库的操作,开发者无需直接操作sql代码,
而是直接操作Java代码来完成对数据库的访问处理。
关于如何基于mybatis的Example代码来访问数据库,请查阅相关资料,
或者参考本模块`org.linlinjava.litemall.db.dservice` 包内的Java代码。
当然,为了达到数据库访问效率,开发者也可以手动自定义mapper文件和对应的Java代码,但目前这里不采用或者不建议采用。
例如,当需要访问两个表的数据时,这里是在业务层通过Java代码遍历的形式来访问两个表。
这里,以`litemall_brand`表举例说明:
1. mybatis generator插件会根据数据库`table`标签
```
```
2. 自动生产src文件夹下domain包内的LitemallBrand.java类、LitemallBrandExample.java类、
dao包内的LitemallBrandMapper.java接口和resources文件夹下dao包内的LitemallBrandMapper.xml文件。
3. 手动在service包内创建LitemallBrandService.java来对外提供具体的服务。
例如,为了得到Brand列表,那么创建list方法,基于前面创建的三个Java来来实现。
```java
@Service
public class LitemallBrandService {
@Resource
private LitemallBrandMapper brandMapper;
public List query(int offset, int limit) {
LitemallBrandExample example = new LitemallBrandExample();
example.or().andDeletedEqualTo(false);
PageHelper.startPage(offset, limit);
return brandMapper.selectByExample(example);
}
}
```
如果基于一个新表创建新访问组件,请阅读下面章节2.2.6
关于mybatis generator的用法,可以参考:
https://blog.csdn.net/isea533/article/details/42102297
### 2.2.2 业务代码
基于2.2.1的代码,业务代码处理一些具体业务相关的操作,对其他模块提供具体的服务。
### 2.2.3 安全代码
### 2.2.4 JSON支持代码
### 2.2.5 配置代码
采用Java注解的方式来完成一些特定的配置操作。
### 2.2.6 新服务组件
本节介绍如果基于一个表创建新的服务组件。
1. 在数据库里面创建一个表,例如`litemall_demo`:
```sql
CREATE TABLE `litemall`.`litemall_demo` (
`id` INT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(45) NULL,
`address` VARCHAR(45) NULL,
PRIMARY KEY (`id`));
INSERT INTO `litemall`.`litemall_demo` (`id`, `name`, `address`)
VALUES ('1', 'hello', 'world');
```
2. 在generatorConfig.xml中增加一个新的table标签
```
```
3. 双击mybatis generator插件,检查LitemallDemo.java类、LitemallDemoExample.java类、
LitemallDemoMapper.java接口和LitemallDemoMapper.xml是否生产。
4. 在service里面新建LitemallDemoService.java类,
```java
@Service
public class LitemallDemoService {
@Resource
private LitemallDemoMapper demoMapper;
public List list() {
LitemallDemoExample example = new LitemallDemoExample();
return demoMapper.selectByExample(example);
}
}
```
5. 可以在`src/test/java/org.linlinjava.litemall.db`包里面创建LitemallDemoTest.java类,
使用Junit进行测试。
```java
@WebAppConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class LitemallDemoTest {
@Autowired
private LitemallDemoService demoService;
@Test
public void test() {
List litemallDemoList = demoService.list();
Assert.assertTrue(litemallDemoList.size() != 0);
}
}
```
6. 同样地,可以在Controller中使用LitemallDemoService来对外提供服务。
```java
@RestController
@RequestMapping("/demo")
public class DemoController {
@Autowired
private LitemallDemoService demoService;
@RequestMapping("/list")
public Object list(){
List demoList = demoService.list();
return demoList;
}
}
```
### 2.2.7 逻辑删除
数据删除可以直接使用delete方法进行物理删除,也可以采用设置删除字段进行逻辑删除。
根据具体业务,也有可能部分数据可以物理删除,部分数据只能逻辑删除。
目前所有删除操作是逻辑删除,除了极少数表外,其他所有表的设置了`deleted 字段。
开发者可以自行修改代码进行真正的物理删除。
### 2.2.8 乐观锁
由于服务是多线程并发的,因此这带来了多线程同时操作数据库中同一数据的问题。
由于数据极少删除或者是逻辑删除,因此操作数据,可以简化成更新数据。
也就是说,需要解决多线程更新数据库同一数据的并发问题。
例如,下单操作中,用户A购买商品G的数量是1个,而用户B同一时间也购买商品G的
数量也是1个,那么如果没有很好地并发控制,有可能商品G的数量仅仅是减1,而不是
设想的2。
通常采用悲观锁或者乐观锁来处理并发更新问题,
本项目目前采用基于`version`字段的乐观锁机制。
原理是:
1. 每个表都存在version字段
2. 更新前,先查询数据,得到表的业务数据和version字段
3. 更新时,通过where条件查询当前version字段和数据库中当前version字段是否相同。
* 如果相同,说明数据没有改变则可以更新,数据更新同时version调整一个新值;
* 如果不相同,则说明数据改变了则更新失败,不能修改数据。
当然,这里好像也存在一个漏洞,3中所比较的数据库当前version字段的值有可能修改过
但是恰巧没有变。例如version加1再减1,那么查询时该值并没有变化。当然,如果version
是单调自增,则应该不存在这个问题。
具体技术细节如下:
1.
当然,由于采用乐观锁,这里也会带来另外一个问题:
数据库有可能更新失败,那么如何处理更新失败的情况?
目前的方法是在业务层多次尝试。
例如:
由于用户A和B同时更新同一商品数量,而用户A成功,B则失败。
此时B失败后会再次进行商品购买逻辑。
当然逻辑上这里仍然会存在再次和其他用户同时购买而失败的情况。
不过考虑到本项目设想的场景,因此可以采用。
开发者需要注意这个问题,可能需要采用其他技术来解决或避免。
### 2.2.9 mybatis增强框架
通过mybatis-generator已经自动生成了很多代码,而且具有一定的功能,
但是开发者仍然需要基于生成的代码写一些固定的CRUD代码。
目前发现已经有两个mybatis增强的框架可以进一步简化代码和功能增强:
* [mybatis-plus](https://github.com/baomidou/mybatis-plus)
* [Mapper](https://github.com/abel533/Mapper)
目前没有采用,以后可能会基于其中之一重构数据库访问代码。
开发者感兴趣的可以自行研究使用。
## 2.3 litemall-core
litemall-core模块是spring boot应用通用的代码,包括配置代码和util代码。
### 2.3.1 config
#### 2.3.1.1 CorsConfig
目前开发过程中,CORS配置是允许所有请求。
真正部署时,开发者需要做一些调整,来保证当前的服务只接受来自所设置域名的请求。
#### 2.3.1.2 GlobalExceptionHandler
如果系统内部产生了异常而开发者没有catch,那么异常的内容会发送到前端。
这里通过提供全局异常处理器,来处理所有开发者没有处理的异常,返回
“系统内部错误”之类的信息给前端从而达到保护系统的效果。
#### 2.3.1.3 JacksonConfig
Jackson做一些设置。
### 2.3.2 util
注意
> 这里的util代码不会涉及具体业务,例如litemall-db模块中存在一个
> OrderUtil类处理数据库中litemall_order表的一些转换工作。
#### 2.3.2.1 ResponseUtil
这里是用于设置response中body的内容格式。
如果是成功则是 :
```json
{
errno: 0,
errmsg: '成功',
data: XXX
}
```
如果失败则是:
```json
{
errno: 非0的XXX,
errmsg: XXX
}
```
#### 2.3.2.2 JacksonUtil
当请求时POST时,请求的json内容在body。
通常存在存在两种方式取出数据:
* 如果json内容正好对应一个POJO,那么在方法中使用POJO时,spring会自动解析填充数据;
* 或者开发者自己采用jackson或者其他json处理库手动解析数据。
这里JacksonUtil简化解析工作。这里代码有局限性,开发者请谨慎使用,或者熟悉Jackson
使用的开发者欢迎优化代码。
#### 2.3.2.3 CharUtil
生成固定长度的随机字母字符串或者随机数字字符串。
#### 2.3.2.4 bcypt
这里是用于对用户密码或者管理员密码加密存储。
bcypt代码本质上是spring里面的代码。
## 2.4 litemall-os-api
对象存储服务目前的目标是支持图片的上传下载。
作为后台模块之一,litemall-os-api并没有对应的前端模块,而只是在litemall-admin模块
的对象存储页面中允许管理员修改。
注意:
> 这个模块是可选的,或者说不建议最终部署时所使用。
> 最终部署时建议采用第三方云存储方案。
### 2.4.1 业务
支持服务:
* 列表
* 创建
* 修改
* 读取
* 删除
* 下载,即下载对象数据文件
* 访问,即直接访问对象数据
### 2.4.2 安全
警告
> 目前这里没有任何安全机制,这意味着任何人如果知道对象存储服务的地址,都可以直接存储访问对象数据。
这样简化的目的是对象存储服务建议最终采用云服务,因此这里仅仅实现一个简单的服务面向测试开发。
如果开发者需要局域网部署,那么这里需要加入一定的安全机制。
### 2.4.3 文件Key
每一个上传的文件都会采用一个随机值key,作为当前文件的网络访问链接的一部分。
以后可能需要进一步支持自定义Key,例如采用原文件名字作为key。
## 2.5 litemall-all
在章节1.5中讨论的部署方案中设计了一种单主机单服务方案,
也就是说三个后台服务和静态文件都部署在一个Spring Boot应用中。
注意:
> 这个模块也是可选的,或者说不是非常建议的,应该仅用在主机内存资源紧张的情况下。
> 最终部署,仍然建议部署多个服务更为安全和稳定。
查看litemall-all模块,代码仅仅只有一个Application类。
实际的原理是litemall-all模块内的pom.xml文件:
1. 声明打包方式是`war`,因此最后会打包war格式
2. 设置`spring-boot-starter-tomcat`包是`provided`,因此最终不会打包
3. 申明依赖`litemall-os-api`、`litemall-wx-api`和`litemall-admin-api`,
因此最终会打包;
在Application类里面通过`scanBasePackages`即可把三个后台服务模块
的服务启动。此外在tomcat中启动,需要采用继承`SpringBootServletInitializer`类
的Application。
4. 利用maven-resources-plugin插件,将litemall-admin模块下编译得到dist文件下的
静态文件打包到static目录中。
注意,这里只是简单的复制。因此开发者需要保证litemall-all打包前,litemall-admin
模块内dist目录下静态文件已经生成。
最终打包以后则是一个war格式的项目包,包含了三个后台服务和静态文件。