项目概要
应用场景:
- 为使用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](/user/inject) WxCloudApi WxCloud
[@if](/user/if) (statPvData == null)
{
图表正在加载中...
}
[@code](/user/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](/user/inject) WxCloudApi WxCloud
[@if](/user/if) (statApplyData == null)
{
图表正在加载中...
}
[@code](/user/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](/user/inject) WxCloudApi WxCloud
[@if](/user/if) (statData == null)
{
图表正在加载中...
}
[@code](/user/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](/user/inject) WxCloudApi WxCloud
[@if](/user/if) (statData == null)
{
图表正在加载中...
}
[@code](/user/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。