【微信支付】分享一个失败的案例
2018-06-04 08:24 by stoneniqiu, 2744 阅读, 29 评论, 收藏, 编辑
这个项目是去年做的,开始客户还在推广,几个月后发现服务器已经关掉了。这是一个发图片猜谜语的应用,用户猜对了分红包,所得奖金可以提现。开发的时候对需求都不太看好,觉得用户粘性太低了。今天就把所有的程序拿了出来放在了github上。供有兴趣的伙伴玩耍。
产品逻辑
用户从公众号直接进来,可以做两件事,一个是发布悬赏谜题,一个是去答题。答题获得赏金可以提现。之前还有聊天和好友功能,觉得太冗余,又拿掉了。用户的悬赏金额来自微信支付或者钱包的余额。客户怎么赚钱呢,有几个点,第一个点是专门去找企业用户,谜题就是广告,答案就是让商品的名字,用户输入一遍答案得到赏金以达到做广告和获得用户的效果,二个是提现有手续费,三个是提现有一定的门槛,比如满了十元才可以提现。四个就是在答对题的页面插广告。谜题是倒计时的,倒时之后剩余赏金会退回到用户的钱包,用户还可以选择重发别人的谜题或者已经过期的谜题。大致就是这样的一个形态。
技术栈
网站基于Asp.net MVC4,相当于是我之前Portal.MVC的一个手机版,不同的是集成了微信支付、微信登录、微信分享和微信的体现与转账。前端用的是zepto+SUI。微信这方面我之前的博客都有介绍过,这次刚好放在一起。界面你们可能不喜欢,这个就另当别论了,我只负责所有代码的实现~ ,下面看下界面吧。
注册
发布的时候确实没有注册和登录两个按键的,为了方便大家在电脑上玩我就放了出来。在公众号里面点击那个微信图标就能登录。客户让我记住状态,免得每次都要登录。我就将用户的openid放到了localstorage里面。然后注册这里用到了阿里大鱼的短信服务,现在应该还有条数,有兴趣的可以玩玩。
出题
原本用的自己的h5插件上传的,但还是有的安卓不支持,最后用了微信提供的上传图片的方法。详情见:http://www.cnblogs.com/stoneniqiu/p/6910326.html,微信的上传还是太麻烦,涉及到上传到微信服务器还要再下载下来,应该用plupload类似的插件去做这样的事情。
悬赏
图片上传后会保存原图和缩略图,然后会让用户确认支付,支付的时候回判断用户钱包的钱是否足够支付这次的悬赏,零钱足够就用零钱,不够就会弹出微信支付。微信支付的js全部在client.js里面。出题完成之后,当别的用户进入谜题就能看到下面的页面。当然在网页端时候这个地方微信支付是不可能成功,需要要在微信环境里面测试。详情可以参考:公众号支付。如果你想在电脑上玩这个,怎么充值呢,答案很简单,那就是去改数据库~~。 数据库中有一个wallets的表.
所以自动动手,丰衣足食。这里的lockMoney就是出题被锁定的金额,当谜题到期了悬赏没有被领取完就会退会到用户的账号。这里是由后台的一个定时服务执行的。具体可以参考下Portal.MVC.Models.Job下的JobCheckQuestionTimeJob这个对象。
提示当前的悬赏,以鼓励用户去答题。谜题有三种模式,平均赏金,递减和一人获奖。
获得赏金
用户如果答对了,就会获得赏金。这个页面下面就是加入了个广告。同样也鼓励用户再去出题。
钱包
用户答对之后就可以获得赏金,这个页面的功能都在UserCenter这个Controller中。设计到支付和提现的都在PaymentController中,提现的顺序是先检查用户提现的金额是否对。现有金额>=提现金额+手续费。然后创建订单,再从数据库划走金额,最后/Checkout/CashTransfers 才是将企业账户的钱转到用户的微信零钱中。
//提现 $(document).on("click", ".cashbt", function () { //验证余额 var money = $("#cashmoney").val(); $.post("/Payment/CheckWalletAndFee", { money: money }, function (res) { if (res.IsSuccess === true) { //创建订单 $.showIndicator(); $.post("/Payment/CreateToCashOrder", { money: money }, function (order) { $.ajax({ url: "/Checkout/CashTransfers", data: { orderNumber: order.OrderId }, type: "post", success: function (result) { if (result.IsSuccess === true) { //处理订单状态并扣除手续费 $.post("/Payment/DealCashFee", { orderNumber: order.OrderId }, function (last) { //提示提现成功! if (last.IsSuccess === true) { $.alert("提现成功,请在查看您的微信零钱!", function () { location.href = "/UserCenter/Index"; }); } else { $.alert(last.Message); } $.hideIndicator(); }); } else { $.hideIndicator(); $.alert(result.Message); } }, error: function (err) { $.hideIndicator(); alert(JSON.stringify(err)); } }); }); } else { $.alert(res.Message); } }); });
提现成功:
具体请参考:企业转账到用户
后端
后端的模块有好几个,做了一些统计,模板还是用的Matrix Admin。
数据库的表如下,MenuStatistics是用来统计访问的页面的。其实也没啥用。其他的是评论,分享,答案,谜题,权限,用户这些表一看也就明白,无需赘述了。
初始化:
由于是codefirst模式,在webconfig中配置好mssql的数据库地址后,可以直接运行。网站起来后,先运行/Install。
public ActionResult Install() { InstallQuestionStrategies(); InstallSystemWallet(); InstallPermission(); InstallAdmin(); return Content("success"); }
这个方法会初始化一些数据。包括策略,系统钱包,权限和管理员。后端管理员Admin,密码admin。你也可以改成你自己喜欢的。如果想要更多测试账号。可以调用InsertTestUsers方法,会创建五个测试用户。如果想要部署到你自己的公众号,除了注册公众号、商户、设置支付地址外,然后就是修改WxPayApi/lib下的Config中的参数了。
小结:虽然是个失败的项目,整理出来供大家玩耍,有问题有想法可以留言。老铁喜欢就扶我一把。
github:https://github.com/stoneniqiu/Protal.MVC.WeiXinPay
最后,喜欢读书的小伙伴可以关注下下面的订阅号~~
你的关注和支持是我写作的最大动力~
书山有路群:452450927
跨域405(Method Not Allowed)问题
2018-05-14 09:23 by stoneniqiu, 70 阅读, 0 评论, 收藏, 编辑zepot post没有问题,用plupload上传出现了这个错误,options过不去。显示Response for preflight has invalid http status code 405
在global中处理下option
protected void Application_BeginRequest() { if (Request.Headers.AllKeys.Contains("Origin") && Request.HttpMethod == "OPTIONS") { Response.End(); } }
另外,还要注意header的设定。多个允许的自定义header逗号隔开。不然也会被拒绝。
<httpProtocol> <customHeaders> <add name="Access-Control-Allow-Origin" value="*" /> <add name="Access-Control-Allow-Headers" value="Content-Type,Token" /> <add name="Access-Control-Allow-Methods" value="GET, POST, PUT, DELETE, OPTIONS" /> </customHeaders> </httpProtocol>
关于IM的一些思考与实践
2018-03-22 23:32 by stoneniqiu, 368 阅读, 1 评论, 收藏, 编辑
上一篇简单的实现了一个聊天网页,但这个太简单,消息全广播,没有用户认证和已读未读处理,主要的意义是走通了websocket-sharp做服务端的可能性。那么一个完整的IM还需要实现哪些部分?
一、发消息
用户A想要发给用户B,首先是将消息推送到服务器,服务器将拿到的toid和内容包装成一个完整的message对象,分别推送给客户B和客户A。为什么也要推送给A呢,因为A也需要知道是否推送成功,以及拿到了messageId可以用来做后面的已读未读功能。
这里有两个问题还要解决,第一个是Server如何推送到客户B,另外一个问题是群消息如何处理?
实现推送
先解决第一个问题,在Server端,每次连接都会创建一个WebSocketBehavior对象,每个WebSocketBehavior都有一个唯一的Id,如果用户在线我们就可以推送过去:
Sessions.SendTo(userKey, Json.JsonParser.Serialize(msg));
需要解决的是需要将用户的Id和WebSocketBehavior的Id关联起来,所以这就要求每个用户连接之后需要马上验证。所以用户的流程如下:
由于JavaScript和Server交互的主要途径就是onmessage方法,暂时不能像socketio那样可以自定义事件让后台执行完成后就触发,我们先只能约定消息类型来实现验证和聊天的区分。
function send(obj) { //必须是对象,还有约定的类型 ws.send(JSON.stringify(obj)) } socketSDK.sendTo = function (toId,msg) { var obj = { toId:toId, content: msg, type: "002"//聊天 } send(obj); } socketSDK.validToken = function (token) { var obj = { content: token || localStorage.token, type: "001"//验证 } send(obj); }
在后端拿到token就可以将用户的guid存下来,所有用户的guid与WebSocketBehavior的Id关系都保存在缓存里面。
var infos = _userService.DecryptToken(token); UserGuid = infos[0]; if (!cacheManager.IsSet(infos[0])) { cacheManager.Set(infos[0], Id, 60); } //告之client验证结果,并把guid发过去 SendToSelf("token验证成功");
调用WebSocketBehavior的Send方法可以将对象直接发送给与其连接的客户端。接下来我们只需要判断toid这个用户在缓存里面,我们就能把消息推送给他。如果不在线,就直接保存消息。
群消息
群是一个用户的集合,发一条消息到群里面,数据库也只需要存储一条,而不是每个人都存一条,但每个人都会收到一次推送。这是我的Message对象和Group对象。
public class Message { private string _receiverId; public Message() { SendTime = DateTime.Now; MsgId = Guid.NewGuid().ToString().Replace("-", ""); } [Key] public string MsgId { get; set; } public string SenderId { get; set; } public string Content { get; set; } public DateTime SendTime { get; set; } public bool IsRead { get; set; } public string ReceiverId { get { return _receiverId; } set { _receiverId = value; IsGroup=isGroup(_receiverId); } } [NotMapped] public Int32 MsgIndex { get; set; } [NotMapped] public bool IsGroup { get; set; } public static bool isGroup(string key) { return !string.IsNullOrEmpty(key) && key.Length == 20; } }
public class Group { private ICollection<User.User> _users; public Group() { Id = Encrypt.GenerateOrderNumber(); CreateTime=DateTime.Now; ModifyTime=DateTime.Now; } [Key] public string Id { get; set; } public DateTime CreateTime { get; set; } public DateTime ModifyTime { get; set; } public string GroupName { get; set; } public string Image { get; set; } [Required] //群主 public int CreateUserId { get; set; } [NotMapped] public virtual User.User Owner { get; set; } public ICollection<User.User> Users { get { return _users??(_users=new List<User.User>()); } set { _users = value; } } public string Description { get; set; } public bool IsDeleteD { get; set; } }
对于Message而言,主要就是SenderId,Content和ReceiverId,我通过ReceiverId来区分这条消息是发给个人的消息还是群消息。对于群Id是一个长度固定的字符串区别于用户的GUID。这样就可以实现群消息和个人消息的推送了:
case "002"://正常聊天 //先检查是否合法 if (!IsValid) { SendToSelf("请先验证!","002"); break; } //在这里创建消息 避免群消息的时候多次创建 var msg = new Message() { SenderId = UserGuid, Content = obj.content, IsRead = false, ReceiverId = toid, }; //先发送给自己 两个作用 1告知对方服务端已经收到消息 2 用于对方通过msgid查询已读未读 SendToSelf(msg); //判断toid是user还是 group if (msg.IsGroup) { log("群消息:"+obj.content+",发送者:"+UserGuid); //那么要找出这个group的所有用户 var group = _userService.GetGroup(toid); foreach (var user in group.Users) { //除了发消息的本人 //群里的其他人都要收到消息 if (user.UserGuid.ToString() != UserGuid) { SendToUser(user.UserGuid.ToString(), msg); } } } else { log("单消息:" + obj.content + ",发送者:" + UserGuid); SendToUser(toid, msg); } //save message //_msgService.Insert(msg); break;
而SendToUser就可以将之前的缓存Id拿出来了。
private void SendToUser(string toId, Message msg) { var userKey = cacheManager.Get<string>(toId); //这个判断可以拿掉 不存在的用户肯定不在线 //var touser = _userService.GetUserByGuid(obj.toId); if (userKey != null) { //发送给对方 Sessions.SendTo(userKey, Json.JsonParser.Serialize(msg)); } else { //不需要通知对方 //SendToSelf(toId + "还未上线!"); } }
二、收消息
收消息包含两个部分,一个是发送回执,一个是页面消息显示。回执用来做已读未读。显示的问题在于,有历史消息,有当前的消息有未读的消息,不同人发的不同消息,怎么呈现呢?先说回执
回执
我定义的回执如下:
public class Receipt { public Receipt() { CreateTime = DateTime.Now; ReceiptId = Guid.NewGuid().ToString().Replace("-", ""); } [Key] public string ReceiptId { get; set; } public string MsgId { get; set; } /// <summary> /// user的guid /// </summary> public string UserId { get; set; } public DateTime CreateTime { get; set; } }
回执不同于消息对象,不需要考虑是否是群的,回执都是发送到个人的,单聊的时候这个很好理解,A发给B,B读了之后发个回执给A,A就知道B已读了。那么A发到群里一条消息,读了这条消息的人都把回执推送给A。A就可以知道哪些人读了哪些人未读。
js的方法里面我传了一个toid,本质上是可以通过message对象查到用户的id的。但我不想让后端去查询这个id,前端拿又很轻松。
//这个toid是应该可以省略的,因为可以通过msgId去获取 //目前这么做的理由就是避免服务端进行一次查询。 //toId必须是userId 也就是对应的sender socketSDK.sendReceipt = function (toId, msgId) {var obj= { toId: toId, content: msgId, type:"003" } send(obj) }
case "003": key = cacheManager.Get<string>(toid); var recepit = new Receipt() { MsgId = obj.content, UserId = UserGuid, }; //发送给 发回执的人,告知服务端已经收到他的回执 SendToSelf(recepit); if (key != null) { //发送给对方 await Sessions.SendTo(key, Json.JsonParser.Serialize(recepit)); } // save recepit break;
这样前端拿到回执就能处理已读未读的效果了。
消息呈现:
我采用的是每个对话对应一个div,这样切换自然,不用每次都要渲染。
当用户点击左边栏的时候,就会在右侧插入一个.messages的div。包括当收到了消息还没有页面的时候,也需要创建页面。
function leftsay(boxid, content, msgid) { //这个view不一定打开了。 $box = $("#" + boxid); //可以先放到隐藏的页面上去, word = $("<div class='msgcontent'>").html(content); warp = $("<div class='leftsay'>").attr("id", msgid).append(word); if ($box.length != 0) { $box.append(warp); } else { $box = $("<div class='messages' id=" + boxid + ">"); $box.append(word); $("#messagesbox").append($box); } }
未读消息
当前页面不在active状态,就不能发已读回执。
function unreadmark(friendId, count) { $("#" + friendId).find("span").remove(); if (count == 0) { return; } var span = $("<span class='unreadnum' >").html(count); $("#"+friendId).append(span); } sdk.on("messages", function (data) { if (sdk.isSelf(data.senderid)) { //自己说的 //肯定是当前对话 //照理说还要判断是不是当前的对话框 data.list = [];//为msg对象增加一个数组 用来存储回执 if (data.isgroup) selfgroupmsg[data.msgid] = data;//缓存群消息 用于处理回执 rightsay(data.content, data.msgid); } else { //别人说的 //不一定是当前对话,就要从ReceiverId判断。 var _toid = data.senderid; if (!sdk.isSelf(data.receiverid)) { //接受者不是自己 说明是群消息 _toid = data.receiverid; } var boxid = _toid + viewkey; //如果是当前会话就发送已读回执 if (_toid == currentToId) { sdk.sendReceipt(data.senderid, data.msgid); } else { if (!msgscache[_toid]) { msgscache[_toid] = []; } //存入未读列表 msgscache[_toid].push(data); unreadmark(_toid, msgscache[_toid].length); } leftsay(boxid, data.content, data.msgid); } });
单聊的时候已读未读比较简单,就判断这条消息是否收到了回执。
$("#" + msgid).find(".unread").html("已读").addClass("ed");
但是群聊的时候,显示的是“几人未读”,而且要能够看到哪些人读了哪些人未读,为了最大的减少查询,在最初获取联系人列表的时候就需要将群的成员也一起带出来,然后前端记录下每一条群消息的所收到的回执。这样每收到一条就一个人。而前端只需要缓存发送的群消息即可。
function readmsg(data) { //区分是单聊还是群聊 //单聊就直接是已读 var msgid = data.msgid; var rawmsg = selfgroupmsg[msgid]; if (!rawmsg) { $("#" + msgid).find(".unread").html("已读").addClass("ed"); } else { rawmsg.list.push(data); //得到了这个群的信息 var ginfo = groupinfo[rawmsg.receiverid]; //总的人数 var total = ginfo.Users.length; //找到原始的消息 //已读的人数 var readcount = rawmsg.list.length; //未读人数 var unread = total - readcount-1;//除去自己 var txt = "已读"; if (unread != 0) { txt = unread + "人未读"; $("#" + msgid).find(".unread").html(txt); } else { $("#" + msgid).find(".unread").html(txt).addClass("ed"); } } }
这样就可以显示几人未读了:
小结:大致的流程已经走通,但还有些问题,比如历史消息和消息存储还没有处理,文件发送,另外还有对于一个用户他可能不止一个端,要实现多屏同步,这就需要缓存下每个用户所有的WebSocketBehavior对象Id。 后续继续完善。
基于WebSocketSharp 的IM 简单实现
2018-03-02 08:56 by stoneniqiu, 1311 阅读, 3 评论, 收藏, 编辑
websocket-sharp 是一个websocket的C#实现,支持.net 3.5及以上来开发服务端或者客户端。本文主要介绍用websocket-sharp来做服务端、JavaScript做客户端来实现一个简单的IM。
WebSocketBehavior
WebSocketBehavior是核心对象,他包含了OnOpen,OnMessage,OnClose,OnError四个方法以及一个Sessions对象。熟悉websocket的都知道前四个方法是用来处理客户端链接、发送消息、链接关闭以及出错。sessions就是用来管理所有的回话连接。每产生一个连接,都会有一个新Id,sessions中会新增一个IWebSocketSession对象。当页面关闭或者刷新都会触发OnClose,继而sessions中会移除对应的IwebSocketSession对象。
WebSocketSessionManager 有一个广播方法:Sessions.Broadcast,通知所有连接的客户端。而WebSocketBehavior中的Send相当于是单发,只能将消息发送到此刻连接的一个客户端。摸清了以上这些我们就可以做一个简单的IM了。
Websoket.Server
新建一个C#控制台程序。现在Nugget中添加websocket-sharp.已经JSON。
然后新增一个Chat类,继承WebSocketBehavior,Chat相当于是一个websocket的服务,你可以创建多个websocketBehavior的实例然后在挂载在websocketServer上。
public class Chat : WebSocketBehavior { private Dictionary<string,string> nameList=new Dictionary<string, string>(); protected override async Task OnMessage(MessageEventArgs e) { StreamReader reader = new StreamReader(e.Data); string text = reader.ReadToEnd(); try { var obj = Json.JsonParser.Deserialize<JsonDto>(text); Console.WriteLine("收到消息:" + obj.content + " 类型:" + obj.type + " id:" + Id); switch (obj.type) { //正常聊天 case "1": obj.name = nameList[Id]; await Sessions.Broadcast(Json.JsonParser.Serialize(obj)); break; //修改名称 case "2": Console.WriteLine("{0}修改名称{1}",nameList[Id],obj.content); Broadcast(string.Format("{0}修改名称{1}", nameList[Id], obj.content),"3"); nameList[Id] = obj.content; break; default: await Sessions.Broadcast(text); break; } } catch (Exception exception) { Console.WriteLine(exception); } //await Send(text); } protected override async Task OnClose(CloseEventArgs e) { Console.WriteLine("连接关闭" + Id); Broadcast(string.Format("{0}下线,共有{1}人在线", nameList[Id], Sessions.Count), "3"); nameList.Remove(Id); } protected override async Task OnError(WebSocketSharp.ErrorEventArgs e) { var el = e; } protected override async Task OnOpen() { Console.WriteLine("建立连接"+Id); nameList.Add(Id,"游客"+Sessions.Count); Broadcast(string.Format("{0}上线了,共有{1}人在线", nameList[Id],Sessions.Count), "3"); } private void Broadcast(string msg, string type = "1") { var data= new JsonDto(){content = msg,type = type,name = nameList[Id]}; Sessions.Broadcast(Json.JsonParser.Serialize(data)); } }
JsonDto
class JsonDto { public string content { get; set; } public string type { get; set; } public string name { get; set; } }
这里用nameList来管理所有的链接Id和用户名称的对应关系,新上线的人都默认为游客。然后再OnMessage中定义了三种消息类型。1表示正常聊天,2表示修改名称。3表示系统通知。用来让前端做一些界面上的区分。
然后在Program中启动WebSocketServer。下面指定了8080端口。
public class Program { public static void Main(string[] args) { var wssv = new WebSocketServer(null,8080); wssv.AddWebSocketService<Chat>("/Chat"); wssv.Start(); Console.ReadKey(true); wssv.Stop(); } }
Client
html:
<div id="messages"> </div> <input type="text" id="content" value=""/> <button id="sendbt">发送</button> <div>昵称:<input type="text" id="nickName" /> <button id="changebt">修改</button> </div>
js:
function initWS() { ws = new WebSocket("ws://127.0.0.1:8080/Chat"); ws.onopen = function (e) { console.log("Openened connection to websocket"); console.log(e); }; ws.onclose = function () { console.log("Close connection to websocket"); // 断线重连 initWS(); } ws.onmessage = function (e) { console.log("收到",e.data) var div=$("<div>"); var data=JSON.parse(e.data); switch(data.type){ case "1": div.html(data.name+":"+data.content); break; case "2": div.addClass("gray"); div.html("修改名称"+data.content) break; case "3": div.addClass("gray"); div.html(data.content) break; } $("#messages").append(div); } } initWS(); function sendMsg(msg,type){ ws.send(JSON.stringify({content:msg,type:type})); } $("#sendbt").click(function(){ var text=$("#content").val(); sendMsg(text,"1") $("#content").val(""); }) $("#changebt").click(function(){ var text=$("#nickName").val(); sendMsg(text,"2") })
运行效果:
是不是很方便~~,喜欢就赞一个。
源码:https://files.cnblogs.com/files/stoneniqiu/websocket-sharp.zip
websocket-sharp:http://sta.github.io/websocket-sharp/
nodejs 实现websocket服务端:http://www.cnblogs.com/stoneniqiu/p/5402311.html
【css3】旋转倒计时
2018-02-23 23:03 by stoneniqiu, 681 阅读, 3 评论, 收藏, 编辑
很多答题的H5界面上有旋转倒计时的效果,一个不断旋转减少的动画,类似于下图的这样。
今天研究了下,可以通过border旋转得到。一般我们可以通过border得到一个四段圆。
See the Pen circle by stoneniqiu (@stoneniqiu) on CodePen.
接下来接可以通过旋转的方式形成一个倒计时的效果:
See the Pen circle-rotate by stoneniqiu (@stoneniqiu) on CodePen.
一开始旋转45度是为了让半圆刚好立起来。然后旋转一百八十度。
.rightcircle{ border-top: .4rem solid #8731fd; border-right: .4rem solid #8731fd; right: 0; transform: rotate(45deg) } .right_cartoon { -webkit-animation: circleProgressLoad_right 10s linear infinite forwards; animation: circleProgressLoad_right 10s linear infinite forwards; } @keyframes circleProgressLoad_right { 0% { -webkit-transform: rotate(46deg); transform: rotate(46deg) } 50%,to { -webkit-transform: rotate(-136deg); transform: rotate(-136deg) } }
毕竟不是真正的减少,要出现一种颜色占大多数就可以通过两个半圆来拼凑。
See the Pen circle-timer by stoneniqiu (@stoneniqiu) on CodePen.
@keyframes circleProgressLoad_left { 0%,50% { -webkit-transform: rotate(46deg); transform: rotate(46deg) } to { -webkit-transform: rotate(-136deg); transform: rotate(-136deg) } }
注意到是右边线转5秒,然后左边再等五秒,这里css动画的效果略有不同,右边是0%开始,50%,to。左边是0%,50%,然后to,这样实现的5秒等待。这就是旋转倒计时的效果,最后还可以通过修改左半环border-left的颜色,来凸显最后几秒钟的紧急情况。
【Html5】-- 塔台管制
2018-01-21 21:22 by stoneniqiu, 607 阅读, 1 评论, 收藏, 编辑
想做这个游戏已久,今天终于初步完成,先解释下,这是一个模拟机场塔台管制指挥的游戏,飞机从不同的方向飞入管制空域,有不同的目的地,飞机名称最后一个字母表示飞机要到达的目的地,分ABCD和R。A-D表示四个方向,R表示到本场的跑道降落。飞机有H,M,S三种速度,离场必须不能是最快的速度(H),降落必须是S的速度这样才能得分。默认设置是20架飞机,最多容量默认是10架飞机。当然实际的指挥比这个要复杂。
基本原理
整个游戏是基于canvas的,纯JavaScript,四种朝向的飞机是用四张图片实现的,所有要不断渲染的对象都在airspace这个数组里面。有Plane,Runway和Exit三个对象。正确指挥一架飞机到目的地有5分。
function Plane(id,sx,sy,heading,url){ this.x=sx; this.y=sy; this.flightId=id; this.h=heading||"down";//up down left right this.img=url||"down.png"; this.draw=drawPlane this.move=movePlane this.speed=airspeed[getRandom(3)]; this.D=destination[getRandom(5)]; this.state="cruise"; this.width=size; this.height=size; this.getCenter=getCenter; }
function Runway(name,x,y,w,h){ this.name=name; this.x=y; this.y=y; this.width=w; this.height=h; this.draw=drawRunway; this.getCenter=getCenter; }
点击捕获
到canvas上选中一架飞机之后会用红色边框,表示当前正在指挥的飞机。canvas本身没有提供对象的click事件
所以要根据鼠标的位置来判断是否选中了目标:
function eventDispature(canvas){ canvas.οnclick=function(e){ console.log(e.offsetX,e.offsetY,e.type) detectEvent(e.offsetX,e.offsetY,e.type) } } function detectEvent(x,y,type){ //判断是否击中 airspace.forEach(function(p){ //范围 x,x+size y,y+size var maX=p.x+p.width; var maY=p.y+p.height; if(x>=p.x&&x<=maX&&y>=p.y&&y<=maY){ p.selected=true; taget=p; console.log("选中",p.flightId,p.x,p.y) airspace.filter(n=>n.flightId!=p.flightId).forEach(n=>n.selected=false); } }) }
根据e.offsetX和e.offsetY获得事件的位置,判断是否在某个飞机的坐标范围里,然后标记选中,并去除其他被标记selected的飞机。当然这个地方还可以完善成一个事件系统,并支持其他的事件。
碰撞检测
碰撞有四种情况,首先是飞机与飞机相撞,飞机飞出边界(是否正确飞向入口),飞机飞入跑道(是否对准入口进入)。错误操作的飞机将会被移除airspace数组。
function isIntersect(p1,p2){ var center=p1.getCenter(); var c1=p2.getCenter(); var dx=Math.abs(center.x-c1.x); var dy=Math.abs(center.y-c1.y); return dx<(p1.width/2+p2.width/2)&&dy<(p1.height/2+p2.height/2) }
三种情况的判断主要依靠上面这个方法,然后再有区分,飞机飞入跑道,首先是坐标矩形会与跑道矩形相交,然后y1,y2在跑道的y轴范围之内即可。
if(isIntersect(plane,runway)&&plane.state==states.cruise){ console.warn(plane.flightId+"进入跑道"); //进入跑道的条件是 左边的两个点 和右边的两个点 var y1=plane.y; var y2=plane.y+plane.height; //速度最慢,方向是跑道才能得分 if(y1>runway.y&&y1<runway.y+runway.height&&y2>runway.y&&y2<runway.y+runway.height &&plane.D==destination[4]&&plane.speed==airspeed[2]) { plane.state=states.landing; score+=5; info(plane.flightId+"正确降落跑道"); showPlaneNum(); plane.state=states.stop; removePlane(plane.flightId); }else{ plane.state=states.crash; info(plane.flightId+"坠毁,航向"+plane.h+",速度"+plane.speed); removePlane(plane.flightId); }
判断进入入口的道理一样。右下角几个按钮分别表示四个方向和三种速度。
不足:
1.飞机用了四张图片还是有点笨,因为当初旋转移动没有搞定,后续继续研究。
2.飞机碰撞的算法还不够准确,离场的判断只判断了一个点。这里是考虑到离场判断和入场飞机有冲突,这里需要再优化下。
3.还可以增加一些效果。
PS:这其实是当时入学时一个测试程序,当时就记住了,今天用前端实现一回。来玩一玩吧,喜欢就给个赞,欢迎拍砖。
git:https://github.com/stoneniqiu/ATC
演示地址:https://stoneniqiu.github.io/tower.html
H5情景意识 --飞机
2018-01-16 11:08 by stoneniqiu, 192 阅读, 0 评论, 收藏, 编辑
当时进入民航大培训前做过一系列的测试,一共是8个小游戏,主要测试情景意识、反应能力、场面控制之类的,有几个还记忆犹新,这个数飞机只是其中之一,今天没事用JavaScript做了一遍。
原理
逻辑比较简单,主要就是通过随机获测试方向,然后添加噪声,三秒后提问。如此循环。
1.获取测试方向
2.获取飞机位置
3.获取噪声方向
4.获取噪声位置。
5.显示飞机。
6.提问
实现
var row=6; var col=6; var headinglist={0:"朝上",1:"朝右",2:"朝下",3:"朝左"}//上下左右 var imglist={0:"plane.png",1:"right.png",2:"down.png",3:"left.png"} var trueHeading; //最多有五架飞机朝左边 var Max=5; //实际朝左的飞机 var realHeading; //朝左边飞机的位置 var reals=[]; //增加干扰的数量 var noiseMax=3; //干扰的方向 var noiseHeading; //获取干扰的位置。 var realnoise; var noise=[]; //创建表格 rander(); function rander(){ //默认是朝上的, var defaultplane="plane.png"; trueHeading=getRandom(4) console.log(headinglist[trueHeading]) //如果选择的是朝上的,那么默认的就朝下。 if(trueHeading==0) defaultplane=imglist[2]; var targetplane=imglist[trueHeading]; $(".title span").html(headinglist[trueHeading]); var $table=$("#table"); $table.empty(); realHeading=getRandom(Max) reals=[]; getRandomPositions(); console.log("realHeading",realHeading); $("#anwser").html("") noise=[]; noiseHeading=getNoiseHeading(trueHeading); getRandom(noiseMax); getRandomNoisePosition(); for(var i=0;i<row;i++){ var $tr=$("<tr>"); for(var j=0;j<col;j++){ //装载飞机 var img=$("<img src='"+defaultplane+"' />") if(IsIn(j,i)){ img=$("<img src='"+targetplane+"' />") } if(IsInNoise(j,i)){ img=$("<img src='"+imglist[noiseHeading]+"' />") } var $td=$("<td>").html(img); $tr.append($td) } $table.append($tr); } setTimeout(function(){ showQuestion(); },3000) } function showQuestion(){ $("#warp").addClass("shadow"); $("#warp").show(); } function close(){ $("#warp").removeClass("shadow"); $("#warp").hide(); // alert(realHeading) $("#anwser").html(realHeading) setTimeout(rander,3000) } function IsIn(x,y){ return !!reals.find(n=>n[0]==x&&n[1]==y); } function IsInNoise(x,y){ return !!noise.find(n=>n[0]==x&&n[1]==y); } function getNoiseHeading(th){ var h=getRandom(4); if(h!=th){ console.log("干扰方向是",headinglist[h]) return h; } return getNoiseHeading(); } function getRandomPositions(){ for(var i=0;i<realHeading;i++){ getRandomPosition(); } } //获取随机噪音的位置 function getRandomNoisePosition(){ var x=getRandom(col); var y=getRandom(row); //检查 var item=reals.find(n=>n[0]==x&&n[1]==y); if(item) return getRandomPosition(); noise.push([x,y]); }; //获取随机的位置 function getRandomPosition(){ var x=getRandom(col); var y=getRandom(row); //检查 var item=reals.find(n=>n[0]==x&&n[1]==y); if(item) return getRandomPosition(); reals.push([x,y]); }; //获取随机数 function getRandom(max){ var ran=Math.round(max*Math.random()); return ran>=max?getRandom(max):ran; } $(".close").click(function(){ close(); })
实现起来很简单,可以通过增加方向数量来增加难度。实际那天测试的时候有八个方向,做得有点懵。 而且还有一道题是四秒钟计算2位数以上的加减乘除,说实话很难反应过来,很多答案都来不及选择。测试完了大家都惴惴不安,后来去问老师成绩,老师笑着说,那种题就是用来吓人的,看你们在遇到打击之后,接下来的反应如何,真是哭笑不得。
git:https://github.com/stoneniqiu/ATC
谈谈转行
2017-12-10 23:11 by stoneniqiu, 2506 阅读, 39 评论, 收藏, 编辑
前几天表弟突然打电话给我说,经过四个月的学习Java,最近拿到了几个offer,不知道选哪家。一问,有老虎证券,摩拜单车,搜狐和滴滴。薪水都是2w+,年薪30万左右。其实这些都蛮不错,最后决定选择了滴滴。
说实话我蛮惊讶的,表弟是北航研究生,毕业两年一直做物联网婴儿方面的创业项目,但因种种原因项目没有继续下去,于是决定找工作。早之前和我聊,说想干程序员,觉得程序员工资高,那时我已有辞职的打算,他笑着说,简直像围城啊,我都想进来,你却要出去。我说,没办法路不一样。他本专业是机械自动化,还有机械设计方面的设计专利。没想到这么快就拿到了这么好的offer。他的简历写的是两年工作经验,且只写了一个项目经验,还是学习java时自己做的一个系统,面试滴滴的时候有好些问题也没有回答上来,但就是过了。
选择比努力重要
其实如果让一个工作几年的程序员去换一种语言,也许很多人就像让他取个丑媳妇一样难为情。过去的沉默成本让他难以抉择。我之前也考虑过要不要学习下java。当时我内心的想法(借口)就是我干嘛要学一个差不多的后端语言呢,其实现在想来,这是两种很不同的语言,结构、用法是相似,但生态(市场)完全不同。打开51job,选择java开发工程师,再选2-3w的范围,拉出来有6页,而别的语言有的只有一页,这说明两点,一个是前者需求大,二个是达到高薪相对于别的语言容易。从投资的角度说,在成本和风险差不多的情况下,当然是选回报率更高的产品。所以表弟就毫不犹豫的选择了java,现在看来也很正确。
代价
我想成本有两个,一个是入行成本,一个是沉没成本。四个月的学习时间,只是一个基本的学习成本。其实换做是我们,给你四个月时间,专门去学习一门语言,如果有其他语言的基础,你可能还不要这么多时间就能上手,因为编程的路子是通的,所以编程的入行成本不高,外行人完全可以自学了入门,周遭都有好些半路来做程序员的,未来肯定有全民编程的时候。区别最大的成本在于告别过去,重新开始。表弟创业结束,对机械兴趣也不大,四个月的学习就当是抛砖引玉。但像我自己,9月底离开上海,现在和一群90后一起正准备期末考试,之前6年的编程经验只能业余耍耍,对未来的工作暂时没有什么帮助,培训一年之后还要实习一年,我的入行成本和沉没成本都比较大,但考虑到长远的未来,也是做出了换行的决定。
起点
在过去的成本中,学历几乎是每个HR眼中的硬通货。大家都见过能力很强但学历不怎么样的人,但人性就是以人的过去判断人,除非你现场证明给他们看,如果有这样的机会的话。像一些能力证书也是很通用的;其实我们在一些商业书籍中经常看到一个人从一个行业转到另一个行业,被委以重任。因为人家之前的成绩很漂亮。所以,不单是学历,就在当前的领域里做出好的成绩,比如在知名的企业、参与过知名的项目、很擅长某些领域,对未来的换行没准也是有帮助的。HR往人群中一扫,先入眼的,自然是那些站的高点的人。同样是基于经验,新行业的内部人士的引荐也可以提高你的起点。
总的说来,过去的已经是既定的,学历出生难以改变。不管考虑不考虑换行,把当前的工作做好都是正确的。对一件事有兴趣不妨花点精力入门学习一下,花不了多少精力,不要老是背负这过去的沉默成本,没准就是未来橄榄枝的必要条件。以上并不是鼓励大家去学java,只是由自己换行和表弟换行想到的一些。