v0.2.0的版本增加了商品库存,在后台添加或编辑商品时可以设置商品的库存数,此外,在前台购买商品时也可以设置需要购买的商品数量。当前版本采用的是付款后才减库存的方式
页面导航:
zenglMall源代码的相关地址:https://github.com/zenglong/zenglMall 当前版本对应的tag标签为: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" 的提示,也就是说数据库已经升级到和代码相匹配的版本了,无需再进行升级了。
当前版本还在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浏览器中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方法将商品详情设置到编辑器中,从而可以解决这个问题。
现实才是唯一真实的东西。
—— 头号玩家