在我们日常的开发过程中,我们常常会遇到要做一些电商或者交易类型的项目,根据客户的需要往往会要求做一些产品竞价的活动。那么怎么设计商品关于竞价的模块,如何设计竞价的表,以及如何使用定时任务去触发状态,如何能保证在多人抢占同一产品时不会出现并列第一的情况.... 一个系统的亮点其实就是在于对核心模块的精准把控。
我们要设计的是满足大多数的竞价表,而不是仅仅满足自己公司的业务。所以我们对表的设计及考虑方面需要更加的详细和周全。
根据业务的基本常识来说,竞价的主题也是围绕着工单作为核心部件。竞价的核心就是在于拍单。
首先我们对于最主要的表可以分为两张表。分别是竞价表和竞价详情表。
首先对于竞价表我们需要设计的属性有很多。这里就不一一罗列出来了,直接将设计的DDL贴出来,大家可以非常直观的感受到。有更好的想法的朋友可以联系我相互学习,完善此表。
CREATE TABLE `auction` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`auction_sn` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '唯一18位竞拍编号供查询,格式为:pyyyymmddhhmmssSSS,年月日时分秒微秒(SSS)',
`title` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '拍卖标题',
`auction_status` int NOT NULL COMMENT '竞拍状态,1 未开拍,2 竞拍中 3 竞拍完成=待确认 4 竞拍失败 5 竞拍结束(已完成)',
`fail_type` int DEFAULT NULL COMMENT '1、无人竞拍 2 、违约 3、达成和解 4、买方已支付违约金 5、卖方已支付违约金',
`fail_time` datetime DEFAULT NULL COMMENT '失败时间',
`price_increase_range` decimal(10,2) NOT NULL COMMENT '加价幅度',
`low_amount` int NOT NULL COMMENT '起拍数量',
`bond` decimal(10,2) NOT NULL COMMENT '保证金',
`total_amount` int NOT NULL COMMENT '总数量',
`start_price` decimal(10,2) NOT NULL COMMENT '起拍价 :单价多少钱,可以往上加价',
`start_time` datetime NOT NULL COMMENT '竞拍开始时间',
`end_time` datetime NOT NULL COMMENT '竞拍结束时间',
`price_display` tinyint NOT NULL COMMENT '是否显示价格,明标暗标 0暗标 1明标',
`auctioneer_id` int NOT NULL COMMENT '发布拍卖的人id,也就是创建人的id',
`final_amount` int DEFAULT NULL COMMENT '成交数量',
`final_total_money` decimal(50,2) DEFAULT NULL COMMENT '成交总金额',
`final_price` decimal(10,2) DEFAULT NULL COMMENT '成交价',
`final_userid` int DEFAULT NULL COMMENT '成交人',
`final_time` datetime DEFAULT NULL COMMENT '成交时间',
`final_id` bigint DEFAULT NULL COMMENT '最终成交拍卖记录表的id',
`econtract_sn` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '唯一19位电子合同编号供查询,格式为:ecyyyymmddhhmmssSSS,年月日时分秒微秒(SSS)',
`create_by` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '' COMMENT '创建人姓名',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '' COMMENT '修改人',
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
`types` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '商品类型',
`breed` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '商品品种',
`role_type` int DEFAULT NULL COMMENT '发布竞价供应者类型 1:个人 2:企业',
`images` varchar(300) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '图片、视频',
`area_id` int DEFAULT NULL COMMENT '服务范围省市区区域id',
`detail_address` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '具体门牌号地址',
`contacts` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '联系人',
`contacts_phone` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '联系人电话',
`longitude` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '经度',
`latitude` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '纬度',
`buyer_defreason` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '买家违约原因',
`seller_defreason` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '卖家违约原因',
`main_pic` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '商品主图',
`buyer_confirm` tinyint DEFAULT NULL COMMENT '买方确认状态 1是true 0是false',
`seller_confirm` tinyint DEFAULT NULL COMMENT '卖方确认状态 1是true 0是false',
`min_weight` decimal(10,2) DEFAULT NULL COMMENT '规格最小重量',
`max_weight` decimal(10,2) DEFAULT NULL COMMENT '规格最大重量',
`avg_weight` decimal(10,2) DEFAULT NULL COMMENT '均重',
`sid` int DEFAULT NULL COMMENT '标题唯一编号',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `auction_sn` (`auction_sn`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='竞拍表';
通过上面的sql我们可以看到竞拍的状态有5种,分别是1 未开拍,2 竞拍中 3 竞拍完成=待确认 4 竞拍失败 5 竞拍结束(已完成)而工单是唯一18位竞拍编号,格式精确到毫秒。
值得注意的是这里设计了买卖双方确认的环节。所以这里的竞拍方式既是支持线上的也是支持线下的。一般线下大宗交易是需要双方都确认后,再根据法律的规定,各自签订电子合同,生效后才具有同等的法律效应。
具体的字段我就不一一的赘述了,有做相关项目需要借鉴的朋友可以直接将sql拷贝出自己研究一下。
我们接下来是对竞价详情表的设计,如下:
CREATE TABLE `auction_detail` (
`id` bigint NOT NULL AUTO_INCREMENT,
`auction_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '竞拍id=auction表里的auction_sn',
`auction_price` decimal(10,2) NOT NULL COMMENT '竞拍价格',
`auction_amount` int NOT NULL COMMENT '竞拍数量',
`auction_time` datetime NOT NULL COMMENT '竞拍时间,精确到毫秒',
`auction_userid` int NOT NULL COMMENT '竞拍人',
`is_transaction` bit(1) DEFAULT NULL COMMENT '是否已此价获拍',
`create_by` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '' COMMENT '创建人',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '' COMMENT '修改人',
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
`detail_address` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '详细地址',
`contacts` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '联系人',
`contacts_phone` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '联系人电话',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='竞拍详情表';
通过详情表,我们可以看到详情表里有一个和主表关联的auction_id字段,然后将竞拍人和他的竞拍时间都放置在了这张表内。并且将最终是否获拍的状态也放置在了这张表之中,说明这张表是一张工单一对多的表。
这里需要说明一下,详细地址设计为一个简单的字符串,这里其实是不规范的,我们相应的最好有一张对应的地区表。可以类似于这样:
如下图:
这种表在网上直接可以获取,国家地理局已经公布了地区表的sql,我们也可以在网上找到一些包含区域编号的相关属性。
这样,我们对应的这两张主要核心竞价业务的表就建好了,接着我们简单的说一下操作的流程。我们首先发布竞价。这里发布的竞价其实就是发布一个商品,不同的是竞价单带有时效性。
简单的贴一下代码吧:
Log(title = "【发布竞价】", businessType = BusinessType.INSERT)
@PostMapping
@RepeatSubmit
public AjaxResult add(@RequestBody Auction auction)
{
String paramString = "发布竞价入参:" + JSON.toJSON(auction).toString();
try {
operLogService.addAuctionLog(paramString,auction.getUserid());
}catch (Exception e){
logger.error("addAuctionLog error:"+e.getMessage());
}
if (StringUtils.isEmpty(auction.getMainPic())){
throw new ServiceException("竞价的主图不能为空!");
}else {
if (null == auction.getUserid()) {
throw new ServiceException("竞价的用户不能为空!");
} else {
if (auction.getLowAmount() > auction.getTotalAmount()) {
throw new ServiceException("最低竞价数量不得大于拍卖总数量!");
}
....
return toAjax(auctionService.insertAuction(auction));
}
}
}
发布完成之后相当于在action表里插入了一条竞价数据。其商品工单拥有独立的属性和起始时间。
当然对于用户已发布的竞拍单,只有当前发布的用户才可以进行修改和删除操作。这里的删除建议做成逻辑删除,可以设置为可恢复的。
当然我们这设计发布竞价的时候会配套的设置个人中心的模块,个人中心模块我们将竞拍单单独的独立出来。
有竞价中、待确认、已完成、未获拍、竞价失败 等状态,是针对用户自己发布的竞价单以及用户竞价别的商家发布的竞价单的工单管理模块。
这里不做也不需做过多的阐述,这里的业务就是将与用户相关的竞价单按照这5种状态从表里查询出来并展示出来。
eg:
// 我的发布
@PostMapping("/myPublishs")
public TableDataInfo myPublish(@RequestBody AuctionParam auctionParam)
{
if (null == auctionParam.getUserid()) {
throw new ServiceException("用户不允许为空!");
}
PageInfo<Auction> list = auctionService.getAllMyPublishs(auctionParam.getUserid());
TableDataInfo rspData = new TableDataInfo();
rspData.setCode(HttpStatus.SUCCESS);
rspData.setMsg("查询成功");
rspData.setRows(list.getList());
rspData.setTotal(list.getTotal());
return rspData;
}
// 我接收到的工单
@PostMapping("/myReciveOrder")
public TableDataInfo myReciveOrder(@RequestBody FliterParam fliterParam)
{
if (null == fliterParam.getUserid()) {
throw new ServiceException("用户不允许为空!");
}
PageInfo<Auction> list = auctionService.myReciveOrder(fliterParam);
TableDataInfo rspData = new TableDataInfo();
rspData.setCode(HttpStatus.SUCCESS);
rspData.setMsg("查询成功");
rspData.setRows(list.getList());
rspData.setTotal(list.getTotal());
return rspData;
}
....
而对于已经发布的竞价单我们普通用户包括个人和企业都可以去竞拍。价高者得
这里贴出关于竞拍接口及获取当前个人用户在竞拍中处于的排名状态。对于不满意自己
竞拍的结果也是可以进行追加的。系统将个人用户的历史竞价出价及个人用户最高出价都进行了缓存,对同一个竞价单第二次出价的时候便可直接从上一次出价的最高价开始向上按需求进行加价。
最终在竞价时间到期后如果用户仍然处于最高出价者,则这一单便归他所有。
这里为了避免恶意竞价产生纠纷的情况,同一个用户不得有超过三次以上的竞价资格。这里的业务可以视情况而定。
/**
* 用户竞拍接口
*/
@Log(title = "竞拍接口", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody AuctionDetail auctionDetail)
{
if (null == auctionDetail.getUserid()) {
throw new ServiceException("当前用户不能为空!");
}else {
String type = String.valueOf(sysRoleMapper.selectRoleListByUserId(auctionDetail.getUserid()));
if (type.equals("102")) {
throw new ServiceException("协会角色不允许竞拍!");
} else {
return toAjax(auctionDetailService.insertAuctionDetail(auctionDetail));
}
}
}
@PostMapping("/myRank")
public AjaxResult getMyRankByCates(@RequestBody AuctionParam auctionParam){
List<RankEntity> rankList = auctionDetailService.getMyRankByCates(auctionParam);
if(rankList != null && rankList.size() > 0){
return success(rankList);
}else{
return success("暂时没有出价信息");
}
}
最后讲一下对于工单状态的刷新
这里我个人选用的是xxl_job作为触发定时任务的中间件。在linux服务器上如何安装xxl_job、开放xxl_job 端口以及如何配置xxl_job 这些我就不坐过多的赘述了,自己有需要的可以去xxl_job官方文档上寻求办法。
这里我就以若依自带的RyTask为例子。如果有朋友不满意若依的这一套的,也可以自己去使用其他方法实现,方法其实也是大同小异。
具体的限制和实现的业务,每一家公司的业务上都有不同
大致上的基本如下:
我们将竞价单的工单状态更新已经写好了,那么我们就可以进行最后一个环节的工作了,开启定时任务。
一般情况下,对于竞价的定时任务而言是需要精确到秒的。
所以这里对于定时任务的选用,以及业务的梳理,sql的优化,表的设计,表数据容量,以及表数据过大造成查询性能延时等等,甚至出现线程等待情况,都需要多做考虑。
至此,竞价模块的设计及定时任务刷新的描述就完成了。
有需要的朋友,欢迎@我 相互交流