v0.2.0的版本增加了商品库存,在后台添加或编辑商品时可以设置商品的库存数,此外,在前台购买商品时也可以设置需要购买的商品数量。当前版本采用的是付款后才减库存的方式

    页面导航:

项目下载地址:

    zenglMall源代码的相关地址:https://github.com/zenglong/zenglMall  当前版本对应的tag标签为:v0.2.0

v0.2.0版本新增功能介绍:

    v0.2.0的版本增加了商品库存,在后台添加或编辑商品时可以设置商品的库存数,此外,在前台购买商品时也可以设置需要购买的商品数量。

    当前版本采用的是付款后才减库存的方式,也就是说在下单时不会减少商品的库存,只有在支付成功后才会去减少商品的库存,这样可以防止恶意下单(也就是下了单但没付款,这样的恶意订单一多就会导致库存不足,并让真正的买家无法下单)。

    当前版本还在数据库的订单表中增加了支付时间字段,这样可以知道订单是什么时候完成支付的。

    当前版本增加了update_table.zl的命令行脚本,当代码从v0.1.0版本升级到v0.2.0版本后,可以通过该脚本将数据库表结构也进行升级,从而让数据库表结构与v0.2.0版本的代码相匹配。

    当前版本还增加了order_close.zl的命令行脚本,可以将所有超时的未付款订单都关闭掉,同时将所有超时的待收货订单进行自动确认收货。

商品库存表:

    当前版本在数据库中增加了一个商品库存表,每个商品的库存数都记录在商品库存表中,在install目录内的create_table.zl脚本中可以看到该表结构的定义:

...............................................................................

// 创建商品库存表
if(mysqlQuery(con, "CREATE TABLE goods_store(
	id int NOT NULL AUTO_INCREMENT, 
	gid int NOT NULL DEFAULT '0' COMMENT '商品ID',
	num int NOT NULL DEFAULT '0' COMMENT '商品库存',
	PRIMARY KEY (id),
	KEY `gid` (`gid`)
	) ENGINE=InnoDB DEFAULT CHARSET utf8 COLLATE utf8_general_ci COMMENT='商品库存表'"))
	finish_with_error(con);
endif

...............................................................................

    之所以将商品库存单独放在一个表中,是因为在操作商品库存时,为了防止并发操作导致出现错误的商品库存,需要用到数据库锁,放在单独的表中方便执行加锁的操作,这样在对库存表加锁时,商品主表的操作不会受到太大影响。

    在后台添加和编辑商品时,也增加了商品库存的输入框,用于设置商品的库存数量:

后台设置商品库存

    后台设置的商品库存都会存储到上面提到过的商品库存表中,通过商品ID和商品进行关联。

    在前台购买商品时,也可以输入需要购买的数量,当然购买的数量不能超过商品的库存:

前台购买商品时可以输入购买数量

订单支付时间:

    当前版本在订单表中增加了支付时间字段,购买商品并完成支付后,在收到支付宝的异步通知时,系统就会去设置订单的支付时间。同样可以在install目录的create_table.zl脚本中查看到该字段的数据库定义:

...............................................................................

// 创建订单表
if(mysqlQuery(con, "CREATE TABLE orders (
	.......................................................................
	`pay_time` timestamp NULL DEFAULT NULL COMMENT '订单支付时间',
	.......................................................................
	PRIMARY KEY (id)
	) ENGINE=InnoDB DEFAULT CHARSET utf8 COLLATE utf8_general_ci COMMENT='订单表'"))
	finish_with_error(con);
endif

...............................................................................

    在前台会员中心和管理后台的订单详情页面,都可以看到订单的支付时间信息:

订单详情页面的支付时间信息

付款后减库存:

    上面提到过,当前版本采用的是付款后减库存的方式,也就是只有在支付成功后才会去减少商品的库存。付款后减库存相关的代码位于notify_url.zl脚本中:

...............................................................................

if(ret)
	db = bltArray();
	Mysql.init(db, config, "tpl/error.tpl");
	order_info = Mysql.fetchOne(db, "select id,oid,status,tid,num,gid from orders where oid=" + sort_body_arr['out_trade_no']);
	if(sort_array['trade_status'] == 'TRADE_SUCCESS' && order_info['status'] == 'WAIT_BUYER')
		Mysql.StartTransaction(db);
		// 通过事务和FOR UPDATE语句对商品库存表中gid所对应的记录行加行锁(注意,这里的gid在创建表结构时就设置了索引,所以可以触发InnoDB的行锁,如果gid不是索引,则会触发表锁,将全表都锁住)
		// 通过加锁以保证商品库存操作的原子性,防止并发操作可能导致的错误的商品库存
		store = Mysql.fetchOne(db, "select gid,num from goods_store where gid = '" + order_info['gid'] + "' FOR UPDATE");
		order_info['status'] = 'WAIT_SELLER_SEND';
		order_info['tid'] = sort_body_arr['trade_no'];
		order_info['update_time'] = order_info['pay_time'] = bltDate('%Y-%m-%d %H:%M:%S');
		Mysql.Update(db, 'orders', order_info, 'id=' + order_info['id']);
		// 采用付款后减库存方式
		store['num'] -= order_info['num'];
		store['num'] = store['num'] < 0 ? 0 : store['num'];
		Mysql.Update(db, 'goods_store', store, 'gid=' + order_info['gid']);
		Mysql.Commit(db);
	endif
	// 验签成功,则返回success
	retval = 'success';
else
	// 验签失败,则返回fail
	retval = 'fail';
endif

...............................................................................

    可以看到,在完成支付并收到支付宝的异步通知后,就会去减少商品的库存,也就是将goods_store商品库存表中该商品的库存数减少。

    从上面的脚本中还可以看到,在减少商品库存时,通过事务和FOR UPDATE语句对库存表中商品ID所对应的记录行加了行锁,从而可以防止并发操作可能导致的错误的商品库存。

更新升级表结构:

    如果是从旧的v0.1.0的版本更新代码到v0.2.0版本的话(通过 git pull ... 命令可以直接将代码进行更新),还需要将数据库表也进行升级。

    当前版本在根目录中增加了一个cmd的子目录,该目录中存放的脚本都只能在命令行中运行,在该目录内存放了一个update_table.zl的脚本,该脚本就可以将表结构从v0.1.0版本升级到v0.2.0(如果以后还发布了更新的版本的话,例如v0.3.0,v0.4.0之类的版本的话,还可以直接升级到更新的版本),该脚本的代码如下:

inc 'common.zl';

// 该脚本会根据update.lock中记录的更新用的源版本号,以及配置文件中的当前代码版本号,对数据库表结构进行升级,从而让数据库的表结构能够和当前版本的代码相匹配
update_lock_file = 'update.lock';

ret = bltReadFile(update_lock_file, &update_version);

// 如果没有update.lock文件,则将更新的源版本号设置为0.1.0,表示从0.1.0版本开始进行更新升级
if(ret != 0)
	update_version = '0.1.0';
endif

// 如果更新的源版本号,和当前的代码版本号相同,则无需进行更新,给出相关提示后,直接退出脚本
if(bltVersionCompare(update_version, config['version']) == 0)
	print "update_version (" + update_version + ") == config['version'], no need to update";
	bltExit();
else
	print "update_version (" + update_version + ")";
endif

querys = rqtGetQuery();

// 如果更新的源版本号小于等于0.1.0的版本,则创建商品库存表,同时在orders订单表中增加pay_time即订单支付时间字段
if(bltVersionCompare(update_version, '0.1.0') <= 0)
	print "update from 0.1.0: ";
	// 创建商品库存表
	Mysql.Exec(db, "CREATE TABLE goods_store(
		id int NOT NULL AUTO_INCREMENT, 
		gid int NOT NULL DEFAULT '0' COMMENT '商品ID',
		num int NOT NULL DEFAULT '0' COMMENT '商品库存',
		PRIMARY KEY (id),
		KEY `gid` (`gid`)
		) ENGINE=InnoDB DEFAULT CHARSET utf8 COLLATE utf8_general_ci COMMENT='商品库存表'");
	print 'create table goods_store';

	// orders表增加pay_time字段
	Mysql.Exec(db, "ALTER TABLE orders ADD COLUMN `pay_time` timestamp NULL DEFAULT NULL COMMENT '订单支付时间' AFTER `update_time`");
	print 'add column pay_time to orders table';

	// 可以通过store_num的命令行参数来设置商品库存表中每个商品的初始库存,如果没有在命令行中指定该参数的话,那么初始库存就是0
	store_num = bltInt(querys['store_num']);
	store_num = store_num < 0 ? 0 : store_num;
	print 'store_num: ' + store_num;
	// 初始化商品库存表
	Mysql.Exec(db, "INSERT INTO goods_store (gid, num) SELECT id," + store_num + " FROM goods");
	print 'init goods_store table';
	print '----------------------';
endif

bltWriteFile(update_lock_file, config['version']);

print 'update table from ' + update_version + ' to ' + config['version'];

    该脚本在命令行中的执行情况类似如下:

[root@192 zenglServer]# ./zenglServer -r "/cmd/update_table.zl?store_num=30"
now in cmd
update_version (0.1.0)
update from 0.1.0: 
create table goods_store
add column pay_time to orders table
store_num: 30
init goods_store table
----------------------
update table from 0.1.0 to 0.2.0
[root@192 zenglServer]# ./zenglServer -r "/cmd/update_table.zl?store_num=30"
now in cmd
update_version (0.2.0) == config['version'], no need to update
[root@192 zenglServer]# 

    通过上面脚本从v0.1.0升级到v0.2.0时,会创建goods_store商品库存表,还会在orders订单表中增加pay_time即支付时间字段。

    此外,可以向update_table.zl脚本传递一个store_num的参数,该参数会将库存表中所有商品的库存都初始化为指定的值,例如上面传递给脚本的store_num的值为30,因此,所有商品的初始库存都是30。

    在完成升级操作后,update_table.zl脚本会将升级后的版本号写入update.lock文件中(例如上面脚本执行后,update.lock文件中记录的值就会是0.2.0),下次再次执行该脚本时,就会从update.lock记录的版本号开始进行升级,如果已经升级到和代码相匹配的版本后,就不会再执行更新操作了,例如,上面再次执行脚本时,就给出了 "update_version (0.2.0) == config['version'], no need to update" 的提示,也就是说数据库已经升级到和代码相匹配的版本了,无需再进行升级了。

order_close.zl脚本:

    当前版本还在cmd目录中增加了order_close.zl的脚本,通过该脚本可以将所有超时的未付款订单都关闭掉,同时将所有超时的待收货订单进行自动确认收货。该脚本的代码如下:

inc 'common.zl';

// 通过该脚本可以将所有超时的未付款订单都关闭掉,同时将所有超时的待收货订单进行自动确认收货
querys = rqtGetQuery();

// 可以在命令行中通过传递close_day参数来设置未付款订单的关闭超时时间,以天为单位,最少是3天
close_day = bltInt(querys['close_day']);
close_day = close_day < 3 ? 3 : close_day;

// 可以在命令行中通过传递confirm_day参数来设置自动确认收货的时间,以天为单位,最少是10天
confirm_day = bltInt(querys['confirm_day']);
confirm_day = confirm_day < 10 ? 10 : confirm_day;

print 'time: ' + bltDate("%Y-%m-%d %H:%M:%S");
print 'close_day: ' + close_day;
print 'confirm_day: ' + confirm_day;

// 关闭所有超过了指定天数的未付款订单(由close_day来确定天数)
data['status'] = 'CLOSE';
data['update_time'] = bltDate("%Y-%m-%d %H:%M:%S");
Mysql.Update(db, 'orders', data, "(`status` = 'WAIT_BUYER') AND (`create_time` < (NOW() - INTERVAL " + close_day + " DAY))");
print "update table `orders` to close WAIT_BUYER order, affected rows: " + Mysql.AffectedRows(db);

// 将所有超过了指定天数的待收货订单都设置为已收货状态(由confirm_day来确定天数)
data['status'] = 'BUYER_CONFIRM';
data['confirm_time'] = data['update_time'] = bltDate("%Y-%m-%d %H:%M:%S");
Mysql.Update(db, 'orders', data, "(`status` = 'WAIT_BUYER_CONFIRM') AND (`send_time` < (NOW() - INTERVAL " + confirm_day + " DAY))");
print "update table `orders` to confirm WAIT_BUYER_CONFIRM order, affected rows: " + Mysql.AffectedRows(db);
print '----------------------------------- ';

    以上脚本在命令行中的执行情况类似如下:

[root@192 zenglServer]# ./zenglServer -r "/cmd/order_close.zl"
now in cmd
time: 2021-02-18 13:48:07
close_day: 3
confirm_day: 10
update table `orders` to close WAIT_BUYER order, affected rows: 0
update table `orders` to confirm WAIT_BUYER_CONFIRM order, affected rows: 0
----------------------------------- 
[root@192 zenglServer]# ./zenglServer -r "/cmd/order_close.zl?close_day=5&confirm_day=12"
now in cmd
time: 2021-02-18 13:48:21
close_day: 5
confirm_day: 12
update table `orders` to close WAIT_BUYER order, affected rows: 0
update table `orders` to confirm WAIT_BUYER_CONFIRM order, affected rows: 0
----------------------------------- 
[root@192 zenglServer]# 

    从上面可以看到,可以通过close_day参数来设置未付款订单的超时天数,还可以通过confirm_day参数来设置自动确认收货的天数。

    我们还可以将order_close.zl脚本加入到crontab计划任务中,这样就可以让他在指定时间启动并自动关闭超时的未付款订单,和自动确认收货了,crontab计划任务类似如下所示:

[root@192 ~]# crontab -l
* * * * * cd /root/zenglServerTest; ./zenglServer -c config_mall.zl -r "/cmd/order_close.zl" >> /root/zenglMall/logs/order_close.log 2>&1
[root@192 ~]# 

IE11中CK编辑器的兼容问题处理:

    当前版本处理了IE11浏览器中CKEditor编辑器的兼容问题,之前的版本在IE11浏览器中使用CK编辑器编辑商品信息时,会出现图片显示不出来,或者丢失内容等问题,当前版本对该问题进行了处理,主要修复代码位于 admin/tpl/goods_add.tpl 模板文件中:

CKEDITOR.replace( 'content' ,{
	height: 300,
	filebrowserUploadUrl: 'upload.zl?act=ckImage',
	filebrowserUploadMethod: 'form',
	tabSpaces: 4
});

// for ie10 and ie11
CKEDITOR.instances.content.setData(datas.posts ? datas.posts.content: '');

    上面在使用CKEDITOR的replace方法替换了文本域后,还会再次通过CKEDITOR.instances.content.setData方法将商品详情设置到编辑器中,从而可以解决这个问题。

结束语:

    现实才是唯一真实的东西。

—— 头号玩家

 

上下篇

下一篇: zenglMall v0.3.0 采用前后端完全分离模式

上一篇: zenglMall v0.1.0 使用zengl语言开发商城系统

相关文章

zenglMall v0.3.0 采用前后端完全分离模式

zenglMall v0.1.0 使用zengl语言开发商城系统

zenglMall v0.4.0 增加商品属性和商品规格