个人案例
- 依可日记
依可日记是我为孩子开发的成长日记,欢迎大家体验
依可日记扫码体验
- 超级密码箱
提供方便的密码查看管理。
超级密码箱扫码体验
- #小程序云开发挑战赛#快速找工作-mrhuostudio
项目概要 应用场景: 为使用C#/dotnet技术的用户展现小程序开发的统一解决方案,让c#程序员更容易的开发微信小程序。为用户在微信中快速找工作,汇聚各大招聘网站的招聘数据,提供集成的岗位查找。用户人群: 各大高校学生、职场人士使用dotnet技术的程序员首先感谢官方提供了这次比赛。刚好最近看到腾讯云云函数SCF 推出Custom Runtime定制化运行功能,正在使用C#封装Custom Runtime的云函数,需要找一个场景来进行验证工作,当前微信小程序使用js 作为开发语言, 云开发的云函数也支持多种语言,但是他们都不支持C#,腾讯云 SCF Custom Runtime的云函数可以让更多的后端程序员投身全栈开发。 本次参加这次大赛的初衷是以大赛的要求来充分验证使用C#打造全场景的云原生应用开发,参加比赛的场景是使用小程序快速查找各大招聘网站的岗位,用户在小程序种输入岗位关键词和城市【支持全部城市岗位(不选城市)】,将查询条件提交给后端云函数向各大招聘网站提交查询请求,合并查询结果返回给小程序,同时将相同查询条件的请求使用Redis缓存到服务端。用户在小程序端可以保存他感兴趣的岗位,也可以在微信小程序种发起申请岗位(调用招聘网站的小程序,这一部分理论上技术可行,更多的是商务,因此目前未实现)。用户在小程序端的相关操作进行一个访问统计,用户登录和保存感兴趣岗位数据,以及用户的访问统计数据使用云开发的云函数 封装api和数据库保存数据, 同时将云开发的数据通过Custom Runtime的云函数进行封装成API 提供给 运营站点进行展示, 运营站点使用基于WebAssembly技术的前端框架blazor 进行开发,通过云开发部署到静态网站托管。 这个项目充分调用了小程序的能力,并且结合云开发的优势,同时扩展云开发的云函数,当前云开发的云函数不支持Custom Runtime,云开发的云函数也是基于腾讯云SCF封装。微信小程序可以在ios和android上运行,解决了移动App必须去兼容多端的接口,减少工作量,开发出来的小程序稳定,用完就走,方便用户使用。 云开发进一步的简化了微信小程序的开发,真正做到云原生应用开发,当前云开发不支持C#语言,本项目的主要目的就是展示使用C#语言在云开发的应用。 项目地址本项目基于MIT协议在Github开源,开源地址https://github.com/dotnetcloudbase/findjobtclooud , 后期在这项目基础上将SCF Custom Runtime 云函数的C#版本继续发展,本项目只实现httptrigger的MVP实现。 演示视频 https://v.qq.com/x/page/x3151ge7pvs.html 项目架构功能架构项目 功能上由小程序 和运营网站构成, 用户使用微信小程序快速找工作,相关的运营数据保存在云开发的云存储中,通过运营网站进行展示。 [图片] 系统架构综合使用云开发 和腾讯云云函数打造Serverless的云原生开发。 [图片] 效果截图小程序[图片] 默认界面是职位搜索界面,输入关键词后,点击键盘上的搜索按钮。小程序将向远程 scf 云函数发起请求,返回职位列表。如下: [图片] 可以看到相关职位都已经列出来,搜索框下面有两个下拉选择,分别是地区选择,和职位来源选择。不选择则返回所有地区和所有来源网站的职位信息。点击地区选择时: [图片] 这里可以切换地区,选中地区确认后,则只筛选指定地区的职位信息。这里可以看到左侧有个“定位”按钮,定位时,将根据 wx.getLocation API 获取用户当前经纬度,然后调用 scf 云函数返回用户城市。达到定位效果。重置按钮,则重置筛选条件,将城市设置为不限制。 [图片] 来源列表里这里支持“猎聘网”、“智联招聘”、“前程无忧”、“Boos直聘”这4个来源。默认不限来源。 [图片] 进入详情界面,可以看到最下方有 “分享”、“收藏”、“申请职位” 三个功能按钮。 点击分享,可以分享职位信息给朋友。 收藏按钮,可以收藏在我的收藏列表里,在“我的收藏”界面里展示。 申请职位按钮可以通过跳转别的网站小程序来实现,但是因为需要商务方面的工作,所以暂时没有实现申请职位。将来有机会,这个功能会实现的。 此详情页面设置了可以分享到朋友圈,点击右上角的 ”...“ 就可以分享到朋友圈。 [图片] “我的”界面里主要有收藏列表功能,用户首次使用时,头像那里有个“立即登录”,登录后,数据将通过上方所示云函数保存起来。 [图片] 这里是我的收藏界面。 关于界面介绍写的很简单。不过这个小程序却不简单。能实现快速的筛选实时岗位信息,想要找工作的你,是否需要? 运营网站 运营网站首页 [图片] 用户访问统计 [图片] 申请职位统计 [图片] 搜索关键词排行榜Top10 [图片] 按城市搜索排行榜Top10 [图片] 功能代码展示SCF CustomRuntime 云函数当前版本云开发不支持C# 编写云函数,SCF云函数支持通过Custom Runtime实现云函数,云开发本身也是基于SCF 云函数实现,因此这个功能主要展示Custom Runtime开发云函数,通过API网关发布给小程序使用。代码比较多,具体内容参见 《在腾讯云云函数计算上部署.NET Core 3.1》。 代码:https://github.com/dotnetcloudbase/findjobtclooud/tree/master/scf namespace Yhd.FindJob { public class JobsHttpFunctionInvoker : HttpFunctionInvoker { private IJobsManager jobsManager; private IAmapWebApi amapWebApi; public JobsHttpFunctionInvoker(ILoggerFactory loggerFactory, IJobsManager manager, IAmapWebApi webApi) : base(loggerFactory) { jobsManager = manager; amapWebApi = webApi; } protected override async Task Handler(SCFContext context, APIGatewayProxyRequestEvent requestEvent) { if (requestEvent != null && requestEvent.RequestContext == null) { return new APIGatewayProxyResponseEvent() { ErrorCode = 410, ErrorMessage = "event is not come from api gateway", }; } if (requestEvent != null) { if (requestEvent.Path != "/api/jobs/getjobs" && requestEvent.Path != "/api/jobs/getdetailsinfo" && requestEvent.Path != "/api/geocode/regeo") { return new APIGatewayProxyResponseEvent() { ErrorCode = 411, ErrorMessage = "request is not from setting api path" }; } if (requestEvent.Path == "/api/jobs/getjobs" && requestEvent.HttpMethod.ToUpper() == "GET") { string sources = requestEvent.QueryString["sources"]; string city = requestEvent.QueryString["city"]; string searchKey = requestEvent.QueryString["searchkey"]; string pageIndex = requestEvent.QueryString["pageindex"]; var jobs = await jobsManager.GetJobsAsync(sources.Split('-').ToList(), city, searchKey, int.Parse(pageIndex)); var response = new APIGatewayProxyResponseEvent() { StatusCode = 200, ErrorCode = 0, ErrorMessage = "", Body = JsonConvert.SerializeObject(jobs), IsBase64Encoded = false, Headers = new Dictionary() }; response.Headers.Add("Content-Type", "application/json"); response.Headers.Add("Access-Control-Allow-Origin", "*"); return response; } if (requestEvent.Path == "/api/jobs/getdetailsinfo" && requestEvent.HttpMethod.ToUpper() == "GET") { string source = requestEvent.QueryString["source"]; string url = requestEvent.QueryString["url"]; var jobs = await jobsManager.GetDetailsInfo(source, url); var response = new APIGatewayProxyResponseEvent() { StatusCode = 200, IsBase64Encoded = false, Headers = new Dictionary() }; if (jobs == null) { response.ErrorCode = -1; response.ErrorMessage = "user code exception caught"; } else { response.ErrorCode = 0; response.ErrorMessage = ""; response.Body = JsonConvert.SerializeObject(jobs); } response.Headers.Add("Content-Type", "application/json"); response.Headers.Add("Access-Control-Allow-Origin", "*"); return response; } if(requestEvent.Path == "/api/geocode/regeo" && requestEvent.HttpMethod.ToUpper() == "GET") { string location = requestEvent.QueryString["location"]; ReGeoParameter reGeoParameter = new ReGeoParameter() { Location = location, Batch = false, Output = "JSON", Radius = 1000, RoadLevel = 0, Extensions = "base", Poitype = string.Empty }; var regeo = await amapWebApi.GetRegeoAsync(reGeoParameter); var response = new APIGatewayProxyResponseEvent() { StatusCode = 200, IsBase64Encoded = false, Headers = new Dictionary() }; if(regeo == null) { response.ErrorCode = -1; response.ErrorMessage = "user code exception caught"; } else { response.ErrorCode = 0; response.ErrorMessage = ""; response.Body = string.IsNullOrEmpty(regeo.ReGeoCode.AddressComponent.City) ? regeo.ReGeoCode.AddressComponent.Province : regeo.ReGeoCode.AddressComponent.City; } response.Headers.Add("Content-Type", "application/json"); response.Headers.Add("Access-Control-Allow-Origin", "*"); return response; } } return new APIGatewayProxyResponseEvent() { ErrorCode = 413, ErrorMessage = "request is not correctly execute" }; } } } namespace Yhd.FindJobStat { /// /// 统计数据云开发数据库调用 /// public class StatsHttpFunctionInvoker : HttpFunctionInvoker { private static WxCloudApi _wxCloudApi; public StatsHttpFunctionInvoker(ILoggerFactory loggerFactory, WxCloudApi wxCloudApi) : base(loggerFactory) { _wxCloudApi = wxCloudApi; } /// /// URL mapping /// private readonly Dictionary>> handlerMapper = new Dictionary>>() { ["GET /api/stat/pv"] = HandlerStatPV, ["GET /api/stat/apply"] = HandlerStatApplyButton, ["GET /api/stat/topsearch"] = HandlerStatTop10Search, ["GET /api/stat/topcity"] = HandlerStatTop10City }; /// /// 根据结果构建统一返回值 /// /// /// /// private static APIGatewayProxyResponseEvent BuildCommonResponse(T responseModel) { var response = new APIGatewayProxyResponseEvent() { StatusCode = 200, ErrorCode = 0, ErrorMessage = "", Body = JsonConvert.SerializeObject(responseModel), IsBase64Encoded = false, Headers = new Dictionary() }; response.Headers.Add("Content-Type", "application/json"); response.Headers.Add("Access-Control-Allow-Origin", "*"); return response; } /// /// 调用 get-stat-pv 云函数 /// /// /// private static async Task HandlerStatPV(APIGatewayProxyRequestEvent requestEvent) { return BuildCommonResponse(await _wxCloudApi.InvokeCloudFunction("get-stat-pv")); } /// /// 调用 get-stat-apply 云函数 /// /// /// private static async Task HandlerStatApplyButton(APIGatewayProxyRequestEvent requestEvent) { return BuildCommonResponse(await _wxCloudApi.InvokeCloudFunction("get-stat-apply")); } /// /// 调用 get-stat-top-search 云函数 /// /// /// private static async Task HandlerStatTop10Search(APIGatewayProxyRequestEvent requestEvent) { return BuildCommonResponse(await _wxCloudApi.InvokeCloudFunction("get-stat-top-search")); } /// /// 调用 get-stat-top-city 云函数 /// /// /// private static async Task HandlerStatTop10City(APIGatewayProxyRequestEvent requestEvent) { return BuildCommonResponse(await _wxCloudApi.InvokeCloudFunction("get-stat-top-city")); } /// /// 拦截并分发请求 /// /// /// /// protected override async Task Handler(SCFContext context, APIGatewayProxyRequestEvent requestEvent) { if (requestEvent == null) { return new APIGatewayProxyResponseEvent() { ErrorCode = 413, ErrorMessage = "request is not correctly execute" }; } if (requestEvent.RequestContext == null) { return new APIGatewayProxyResponseEvent() { ErrorCode = 410, ErrorMessage = "event is not come from api gateway", }; } var path = $"{requestEvent.HttpMethod.ToUpper()} {requestEvent.Path.ToLower()}"; if (!handlerMapper.ContainsKey(path)) { return new APIGatewayProxyResponseEvent() { ErrorCode = 411, ErrorMessage = "request is not from setting api path" }; } return await handlerMapper[path](requestEvent); } } } 云开发云函数代码: https://github.com/dotnetcloudbase/findjobtclooud/tree/master/cloudfunctions 用户登录云函数主要是为了保存用户基本信息数据,如果用户信息已存在,则以最新数据覆盖旧数据,否则保存当前用户基本信息。 const cloud = require('wx-server-sdk') cloud.init() const db = cloud.database() exports.main = async (event, context) => { const wxContext = cloud.getWXContext() const openId = wxContext.OPENID; var user = await db.collection('users').where({ openId: openId }).get(); if (user.data.length != 0) { var userInfo = user.data[0]; var id = userInfo._id; userInfo.avatarUrl = userInfo.avatarUrl || event.userInfo.avatarUrl; userInfo.country = userInfo.country || event.userInfo.country; userInfo.province = userInfo.province || event.userInfo.province; userInfo.city = userInfo.city || event.userInfo.city; userInfo.gender = typeof (userInfo.gender) === "number" ? userInfo.gender : event.userInfo.gender; userInfo.language = userInfo.language || event.userInfo.language; userInfo.nickName = userInfo.nickName || event.userInfo.nickName; userInfo.unionId = wxContext.UNIONID || ""; delete userInfo._id; await db.collection('users').doc(id).update({ data: userInfo }); return userInfo; } else { var userInfo = event.userInfo; userInfo.avatarUrl = userInfo.avatarUrl || null; userInfo.country = userInfo.country || null; userInfo.province = userInfo.province || null; userInfo.city = userInfo.city || null; userInfo.gender = typeof (userInfo.gender) === "number" ? userInfo.gender : 0; userInfo.language = userInfo.language || "zh_CN"; userInfo.nickName = userInfo.nickName || null; userInfo.openId = wxContext.OPENID; userInfo.unionId = wxContext.UNIONID || ""; await db.collection('users').add({ data: userInfo }); return userInfo; } } 获取收藏列表云函数列出了用户已添加的收藏列表。 const cloud = require('wx-server-sdk') cloud.init() const db = cloud.database() exports.main = async (event, context) => { return (await db.collection('favorites').where({ openId: cloud.getWXContext().OPENID }).get()).data; } 保存用户收藏,将职位名称,地区,薪资,来源公司名称都保存在收藏列表里。 const cloud = require('wx-server-sdk') cloud.init() const db = cloud.database() exports.main = async (event, context) => { const openId = cloud.getWXContext().OPENID; var favorite = await db.collection('favorites').where({ openId: openId, url: event.url }).get(); if (favorite.data.length == 0) { var favoriteInfo = { openId: openId, source: event.source, url: event.url, jobTitle: event.jobTitle, area: event.area, salary: event.salary, jobCompany: event.jobCompany, time: event.time, logo: event.logo }; await db.collection('favorites').add({ data: favoriteInfo }); return true } await db.collection('favorites').doc(favorite.data[0]._id).remove(); return false } 是否已收藏云函数就比较简单了,这里只是检查用户是否保存了职位详情 url。 const cloud = require('wx-server-sdk') cloud.init() const db = cloud.database() exports.main = async (event, context) => { const openId = cloud.getWXContext().OPENID; var favorite = await db.collection('favorites').where({ openId: openId, url: event.url }).get(); return favorite.data.length > 0; } 这里我们把统计数据归了类,用 type 来分类,type 一共有:"preview" 预览, "click" 点击详情, "location" 定位, "search" 搜索, "apply" 申请职位按钮, "favorite" 收藏, "shareToFriend" 分享给朋友, "shareToTimeline" 分享朋友圈这8类数据。extra 是附加数据结构,在以下的代码中会详细介绍。 const cloud = require('wx-server-sdk') cloud.init() const db = cloud.database() exports.main = async (event, context) => { var statInfo = { date: event.date, type: event.type, page: event.page, extra: event.extra }; await db.collection('stats').add({ data: statInfo }); return true } 6、剩下的云函数:get-stat-apply,get-stat-pv,get-stat-top-city,get-stat-top-search, 这4个云函数是获取统计数据用的。这里只贴 get-stat-top-search 作为实例,其他云函数类似。 const cloud = require('wx-server-sdk') cloud.init() const db = cloud.database() const $ = db.command.aggregate exports.main = async () => { return (await db.collection('stats') .aggregate() .match({ type: "search" }) .group({ _id: $.toLower('$extra.keyword'), count: $.sum(1) }) .sort({ count: 1, }) .limit(10) .end()).list; } 在这个云函数里以 {type: "search"} 作为查询条件,然后以关键词分组,数量从大到小排序,取了前10条数据作为结果。 小程序 代码: https://github.com/dotnetcloudbase/findjobtclooud/tree/master/miniprogram 小程序界面主要使用了 ColorUI 开源的 UI 框架,这个框架最大的特点是漂亮、纯 css 框架, 推荐大家使用并 star。 小程序页面比较简单,结构代码不贴了,主要贴一下 jobService.js 这个类。代码比较多。 const CONST = require("./consts") let _service = (function () { const API_BASE = "https://xxx/release" const API_GET_JOBS = API_BASE + "/api/jobs/getjobs" const API_GET_JOB_DETAIL = API_BASE + "/api/jobs/getdetailsinfo" const API_GET_LOCATED_CITY = API_BASE + "/api/geocode/regeo" const DEBUG_ENABLED = false let loggerInfo = function (msg, param) { console.log("INFO: " + msg, param) }; let loggerError = function (msg, param) { console.error("ERROR: " + msg, param) if (DEBUG_ENABLED) { wx.showModal({ showCancel: false, content: `${msg}${!param ? "" :"\n" + JSON.stringify(param)}` }) } }; let dateFormat = function (fmt, date) { date = date || new Date() let ret; let opt = { "y+": date.getFullYear().toString(), // 年 "M+": (date.getMonth() + 1).toString(), // 月 "d+": date.getDate().toString(), // 日 "H+": date.getHours().toString(), // 时 "m+": date.getMinutes().toString(), // 分 "s+": date.getSeconds().toString() // 秒 }; for (let k in opt) { ret = new RegExp("(" + k + ")").exec(fmt); if (ret) { fmt = fmt.replace(ret[1], (ret[1].length == 1) ? (opt[k]) : (opt[k].padStart(ret[1].length, "0"))) }; }; return fmt; }; let getJobs = function (keyword, sources, city, pageIndex, callback) { wx.request({ url: API_GET_JOBS, method: "GET", data: { pageindex: pageIndex, sources: sources, city: city, searchkey: keyword }, success(res) { loggerInfo(`请求接口【${API_GET_JOBS}】成功`, res) var data = JSON.parse(res.data.Body || "[]") || []; var jobs = data.map(p => { var logo = "" var source = "" switch (p.Source) { case "Liepin": logo = "/images/logo_liepin.png" source = "猎聘网" break; case "ZLZhaopin": logo = "/images/logo_zhilian.png" source = "智联招聘" break; case "QC51": logo = "/images/logo_qianchengwuyou.png" source = "前程无忧" break; case "BOSS": logo = "/images/logo_boss.png" source = "BOSS直聘" break; } var sourceValue = CONST.SITES.filter(p => p.name == source)[0].value; return { "logo": logo, "source": source, "jobTitle": (p.PositionName || "").trim(), "salary": (p.Salary || "").trim(), "jobCompany": (p.CorporateName || "").trim(), "area": (p.WorkingPlace || "").trim(), "time": (p.ReleaseDate || "").trim(), "detailsUrl": (p.DetailsUrl || "").trim(), "sourceValue": sourceValue } }); callback && callback(jobs) }, fail(res) { callback && callback([]) loggerError("获取职位列表出错!", res) } }) }; let getJobDetail = function (source, url, callback) { wx.request({ url: API_GET_JOB_DETAIL, method: "GET", data: { source: source, url: url }, success(res) { loggerInfo(`请求接口【${API_GET_JOB_DETAIL}】成功`, res) if (!res.data.Body) { callback && callback(null) return; } callback && callback(JSON.parse(res.data.Body)) }, fail(res) { callback && callback(null) loggerError("获取职位详情出错!", res) } }) }; let getCityByLatlng = function (lat, lng, callback) { wx.request({ url: API_GET_LOCATED_CITY, method: "GET", data: { location: `${lng},${lat}` }, success(res) { loggerInfo(`请求接口【${API_GET_LOCATED_CITY}】成功`, res) if (!res.data.Body) { callback && callback(null) return; } callback && callback(res.data.Body) }, fail(res) { callback && callback(null) loggerError("反解析地理位置失败!", res) } }) }; let isFavorite = function (url, callback) { wx.cloud.callFunction({ name: 'is-favorite', data: { url: url } }).then(res => { loggerInfo(`调用云函数【is-favorite】成功`, res) callback && callback(res.result); }).catch(err => { callback && callback(false); loggerError("调用云函数[is-favorite]失败!", err) }); }; let saveFavorite = function (jobInfo, callback) { wx.cloud.callFunction({ name: 'save-favorite', data: jobInfo }).then(res => { loggerInfo(`调用云函数【save-favorite】成功`, res) callback && callback(res.result); }).catch(err => { callback && callback(null); loggerError("调用云函数[save-favorite]失败!", err) }); }; let deleteFavorite = function (url, callback) { wx.cloud.callFunction({ name: 'save-favorite', data: { url: url } }).then(res => { loggerInfo(`调用云函数【save-favorite】成功`, res) callback && callback(res.result); }).catch(err => { callback && callback(null); loggerError("调用云函数[save-favorite]失败!", err) }); }; let getMyFavorites = function (callback) { wx.cloud.callFunction({ name: 'user-favorite' }).then(res => { loggerInfo(`调用云函数【user-favorite】成功`, res) callback && callback(res.result || []); }).catch(err => { callback && callback([]); loggerError("调用云函数[user-favorite]失败!", err) }); }; //统计方法 const EventType = { PREVIEW: "preview", CLICK: "click", LOCATION: "location", SEARCH: "search", APPLY: "apply", FAVORITE: "favorite", SHARE_TO_FRIEND: "shareToFriend", SHARE_TO_TIME_LINE: "shareToTimeline" } let stat = function (page, event, extras) { try { setTimeout(() => { var date = dateFormat("yyyy-MM-dd") console.log(`[${date}] STAT: [${event}] ${page}`, extras) wx.cloud.callFunction({ name: 'post-stat', data: { date: date, type: event, page: page, extra: extras } }).then(res => { loggerInfo(`调用云函数【post-stat】成功`, res) }).catch(err => { loggerError("调用云函数[post-stat]失败!", err) }); }, 100); } catch (error) { console.warn("POST STATISTIC DATA ERROR, ", error); loggerError("调用云函数[post-stat]失败!", error) } } let statPreviewJobIndex = function () { stat("/pages/job/index", EventType.PREVIEW) }; let statPreviewJobDetail = function () { stat("/pages/job/detail", EventType.PREVIEW) }; let statPreviewMy = function () { stat("/pages/job/my", EventType.PREVIEW) }; let statPreviewMyFavorite = function () { stat("/pages/my/favorites", EventType.PREVIEW) }; let statPreviewAbout = function () { stat("/pages/my/about", EventType.PREVIEW) }; let statUseGetLocation = function () { stat(null, EventType.LOCATION) }; let statSearch = function (keyword, source, city) { stat(null, EventType.SEARCH, { keyword, source, city }) }; let statApplyJob = function (source, url) { stat(null, EventType.APPLY, { source, url }) }; let statFavoriteJob = function (source, url) { stat(null, EventType.FAVORITE, { source, url }) }; let statShareJobToFriend = function (source, url) { stat(null, EventType.SHARE_TO_FRIEND, { source, url }) }; let statShareJobToTimeline = function (source, url) { stat(null, EventType.SHARE_TO_TIME_LINE, { source, url }) }; let getStatPV = function (callback) { wx.cloud.callFunction({ name: 'get-stat-pv' }).then(res => { loggerInfo(`调用云函数【get-stat-pv】成功`, res) callback && callback(res.result || []); }).catch(err => { callback && callback([]); loggerError("调用云函数[get-stat-pv]失败!", err) }); }; let getStatTopSearch = function (callback) { wx.cloud.callFunction({ name: 'get-stat-top-search' }).then(res => { loggerInfo(`调用云函数【get-stat-top-search】成功`, res) callback && callback(res.result || []); }).catch(err => { callback && callback([]); loggerError("调用云函数[get-stat-top-search]失败!", err) }); }; let getStatTopCity = function (callback) { wx.cloud.callFunction({ name: 'get-stat-top-city' }).then(res => { loggerInfo(`调用云函数【get-stat-top-city`, res) callback && callback(res.result || []); }).catch(err => { callback && callback([]); loggerError("调用云函数[get-stat-top-city]失败!", err) }); }; return { getJobs, getJobDetail, getCityByLatlng, isFavorite, saveFavorite, deleteFavorite, getMyFavorites, statPreviewJobIndex, statPreviewJobDetail, statPreviewMy, statPreviewMyFavorite, statPreviewAbout, statUseGetLocation, statSearch, statApplyJob, statFavoriteJob, statShareJobToFriend, statShareJobToTimeline, getStatPV, getStatTopSearch, getStatTopCity }; })(); module.exports = _service loggerInfo,loggerError 两个函数分别是调试代码输出控制台时使用。 getJobs,getJobDetail, getCityByLatlng, isFavorite, saveFavorite, deleteFavorite, getMyFavorites, 这几个函数参与了小程序所有业务。上面截图里都有大概说明。 statPreviewJobIndex,statPreviewJobDetail,statPreviewMy,statPreviewMyFavorite,statPreviewAbout,statUseGetLocation,statSearch,statApplyJob,statFavoriteJob,statShareJobToFriend,statShareJobToTimeline,这几个云函数主要是为了提交统计数据所用。 getStatPV,getStatTopSearch,getStatTopCity,这几个云函数获取了实时的统计数据。 第一版我们把统计图表做到了小程序里,但是因为 echart 和 canvas 的一些bug,导致有时候在一个页面里渲染不了3个canvas,所以此功能就被砍掉了。不过当时我保留了一张截图。我们看下: 是不是也很帅气? 运营网站代码: https://github.com/dotnetcloudbase/findjobtclooud/tree/master/blazorsite/FindJobBlazorSite 运营网站界面主要使用了blazor框架,Blazor 借助于WebAssembly技术 改进这种前后端分离的模式,他有两种模式支持:Blazor WebAssembly 应用和Blazor Server ,个人认为Blazor Webassembly 模式的应用才是这种前后端分离的正途。 在 Blazor WebAssembly 应用程序中构建的文件将编译并发送到浏览器。然后,浏览器在浏览器的执行沙盒中运行您的 JavaScript、HTML 和 C#。它甚至运行 .NET 运行时的版本,这个运行时处理 JavaScript 互操作,并提供基本服务(如垃圾回收)和更高级别的功能(布局、路由和用户界面小部件等), Blazor 允许您使用 C# 而不是 JavaScript 构建交互式 Web UI, Blazor 应用由使用 C#、HTML 和 CSS 实现的可重用 Web UI 组件组成, 客户端和服务器代码都用 C# 编写,允许您共享代码和库. 用户访问统计 @inject WxCloudApi WxCloud @if (statPvData == null) { 图表正在加载中... } @code { LineConfig chartConfig = new LineConfig { Height = 650, Title = new Title { Visible = true, Text = "FindJob 访问统计" }, Description = new Description { Visible = true, Text = "本图表统计30天内 FindJob 访问情况", }, ForceFit = true, Padding = "auto", XField = "_id", YField = "pv", Meta = new { _id = new { Alias = "日期" }, pv = new { Alias = "PV" } }, Label = new ColumnViewConfigLabel { Visible = true, Style = new TextStyle { FontSize = 12, FontWeight = 600, Opacity = 60, } }, Interactions = new Interaction[] { new Interaction { Type = "slider", Cfg = new { start = 0, end = 1, } } }, Smooth = true }; IChartComponent chartInstance = null; List statPvData = null; protected override async Task OnInitializedAsync() { var tempPv = (await WxCloud.GetStatPv()).ToDictionary(p => DateTime.Parse(p.Date), p => p.Value); var minDate = tempPv.Min(p => p.Key); var maxDate = tempPv.Max(p => p.Key); statPvData = new List(); while (minDate.Date <= maxDate.Date) { if (tempPv.ContainsKey(minDate)) { statPvData.Add(new StatPv() { Date = minDate.ToString("yyyy-MM-dd"), Value = tempPv[minDate] }); } else { statPvData.Add(new StatPv() { Date = minDate.ToString("yyyy-MM-dd"), Value = 0 }); } minDate = minDate.AddDays(1); } await chartInstance.ChangeData(statPvData); } } 申请职位按钮点击统计 @inject WxCloudApi WxCloud @if (statApplyData == null) { 图表正在加载中... } @code { ColumnConfig chartConfig = new ColumnConfig { Height = 650, Title = new Title { Visible = true, Text = "FindJob 申请职位按钮点击统计" }, Description = new Description { Visible = true, Text = "本图表统计30天内 FindJob 申请职位按钮点击情况", }, ForceFit = true, Padding = "auto", XField = "_id", YField = "pv", Meta = new { _id = new { Alias = "日期" }, pv = new { Alias = "点击量" } }, Label = new ColumnViewConfigLabel { Visible = true, Position = "middle", Style = new TextStyle { FontSize = 12, FontWeight = 600, Opacity = 60, } }, Interactions = new Interaction[] { new Interaction { Type = "slider", Cfg = new { start = 0, end = 1, } } } }; IChartComponent chartInstance = null; List statApplyData = null; protected override async Task OnInitializedAsync() { var tempPv = (await WxCloud.GetStatApply()).ToDictionary(p => DateTime.Parse(p.Date), p => p.Value); var minDate = tempPv.Min(p => p.Key); var maxDate = tempPv.Max(p => p.Key); statApplyData = new List(); while (minDate.Date <= maxDate.Date) { if (tempPv.ContainsKey(minDate)) { statApplyData.Add(new StatApply() { Date = minDate.ToString("yyyy-MM-dd"), Value = tempPv[minDate] }); } else { statApplyData.Add(new StatApply() { Date = minDate.ToString("yyyy-MM-dd"), Value = 0 }); } minDate = minDate.AddDays(1); } await chartInstance.ChangeData(statApplyData); } } 搜索关键词Top10排行榜 @inject WxCloudApi WxCloud @if (statData == null) { 图表正在加载中... } @code { BarConfig chartConfig = new BarConfig { Height = 650, Title = new Title { Visible = true, Text = "FindJob 搜索关键词 Top10 排行榜" }, Description = new Description { Visible = true, Text = "本图表统计 FindJob 搜索次数最多的关键词", }, ForceFit = true, Padding = "auto", XField = "count", YField = "_id", Meta = new { _id = new { Alias = "关键词" }, count = new { Alias = "搜索次数" } }, Label = new BarViewConfigLabel { Visible = true, Position = "left" } }; IChartComponent chartInstance = null; IEnumerable statData = null; protected override async Task OnInitializedAsync() { statData = (await WxCloud.GetStatTopSearch()).OrderByDescending(p => p.Value); await chartInstance.ChangeData(statData); } } 搜索城市Top10排行榜 @inject WxCloudApi WxCloud @if (statData == null) { 图表正在加载中... } @code { PieConfig chartConfig = new PieConfig { Height = 650, Title = new Title { Visible = true, Text = "FindJob 搜索城市 Top10 排行榜" }, Description = new Description { Visible = true, Text = "本图表统计 FindJob 搜索次数最多的城市", }, ForceFit = true, Padding = "auto", Meta = new { _id = new { Alias = "城市" }, count = new { Alias = "搜索次数" } }, AngleField = "count", ColorField = "_id", Label = new PieLabelConfig { Visible = true, Type = "inner" } }; IChartComponent chartInstance = null; IEnumerable statData = null; protected override async Task OnInitializedAsync() { statData = (await WxCloud.GetStatTopCity()).OrderByDescending(p => p.Value); await chartInstance.ChangeData(statData); } } 运营网站部署云开发的网站网站托管,地址是 https://findjob-1gcto7ln453287ca-1257277642.tcloudbaseapp.com/ 体验小程序小程序的体验二维码: [图片] 运营网站:https://findjob-1gcto7ln453287ca-1257277642.tcloudbaseapp.com/ 团队介绍我们的团队名mrhuostudio,我们团队是两名.NET程序员组成,我们的目标是在微信小程序、云函数开发中提供一套通用的C#解决方案。 张善友,友浩达科技有限公司 CTO,.NET 技术专家,现任 微软 MVP,腾讯云TVP,华为云MVP,有20年编程经验的程序员。 霍小平,以前一直在不入流的网络公司里做外包服务,现在是全职奶爸,但正是因为做外包服务,对各种技术名词都略有涉及,比如:.net, android, java, php, nodejs, python, openresty + lua, redis, android, elasticsearch, mysql, sqlserver, vue, 各种小程序等等,当然主要以 .net 为主,一个8年的老 .neter。
2020-09-14 - 云存储的图片不能在线处理吗?
比如七牛的图片存储,可以设置返回的大小什么的,云存储图片不能处理吗? 我现在做了一个日记小程序,其中相册功能,如果选择原图上传的图片,一个页面显示太多的话直接 OOM 崩溃了,如果云存储图片有处理功能就好了。 除了上传时压缩图片,请问大家还有什么招能避免OOM吗??
2019-12-29 - 依可日记
此案例完全使用云开发,使用云开发使得开发效率达到了前所未有的高度。强烈推荐大家使用云开发。 此小程序自带管理后台,可以在小程序端管理管理用户和日记,可以给用户分配管理员权限。 需要的同学,可以私聊我。 欢迎使用并提交建议意见给我,使得此产品更加好用!
2019-12-16 - 超级密码箱
此案例完全使用云开发,使用云开发使得开发效率和数据安全达到了前所未有的高度。强烈推荐大家使用云开发。 欢迎使用并提交建议意见给我,使得此产品更加好用!
2019-12-01