评论

#小程序云开发挑战赛#快速找工作-mrhuostudio

我们的团队名mrhuostudio,我们团队是两名.NET程序员组成,我们的目标是在微信小程序、云函数开发中提供一套通用的C#解决方案。

项目概要


应用场景:

  1. 为使用C#/dotnet技术的用户展现小程序开发的统一解决方案,让c#程序员更容易的开发微信小程序。
  2. 为用户在微信中快速找工作,汇聚各大招聘网站的招聘数据,提供集成的岗位查找。

用户人群:

  1. 各大高校学生、职场人士
  2. 使用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({
                showCancelfalse,
                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  
点赞 351
收藏
评论

12 个评论

  • 暗白
    暗白
    2020-09-22

    棒棒的

    2020-09-22
    赞同 9
    回复
  • 风雪夜归人
    风雪夜归人
    发表于小程序端
    2020-09-23
    赞赞赞
    2020-09-23
    赞同 2
    回复
  • 🥀ᮨ໌້顺为 ꧔ꦿ༊
    🥀ᮨ໌້顺为 ꧔ꦿ༊
    发表于小程序端
    2020-09-23
    666
    2020-09-23
    赞同 2
    回复
  • 邹声全-Sanchez
    邹声全-Sanchez
    发表于小程序端
    2020-09-24
    加油!
    2020-09-24
    赞同 2
    回复
  • Simon
    Simon
    发表于小程序端
    2020-09-24
    非常棒滴
    2020-09-24
    赞同 2
    回复
  • 谢骋涛
    谢骋涛
    发表于小程序端
    2020-09-23
    666
    2020-09-23
    赞同 2
    回复
  • Jim
    Jim
    发表于小程序端
    2020-09-23
    劲抽
    2020-09-23
    赞同 2
    回复
  • Echo
    Echo
    发表于移动端
    2020-09-23
    厉害
    2020-09-23
    赞同 2
    回复
  • 小丽
    小丽
    发表于移动端
    2020-09-25
    超级赞👍
    2020-09-25
    赞同 1
    回复
  • 到处逛逛
    到处逛逛
    发表于移动端
    2020-09-25
    棒棒的
    2020-09-25
    赞同 1
    回复

正在加载...

登录 后发表内容