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

    项目概要

    应用场景:

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

    用户人群:

    各大高校学生、职场人士

    1. 使用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开源,开源地址

    后期在这项目基础上将SCF Custom Runtime 云函数的C#版本继续发展,本项目只实现httptrigger的MVP实现。

    演示视频

    https://v.qq.com/x/page/x3151ge7pvs.html

    项目架构

    功能架构

    项目 功能上由小程序 和运营网站构成, 用户使用微信小程序快速找工作,相关的运营数据保存在云开发的云存储中,通过运营网站进行展示。

    20201010013716141

    系统架构

    综合使用云开发 和腾讯云云函数打造Serverless的云原生开发。

    20201010013717760

    效果截图

    小程序

    20201010013732594

    默认界面是职位搜索界面,输入关键词后,点击键盘上的搜索按钮。小程序将向远程 scf 云函数发起请求,返回职位列表。如下:

    20201010013737360

    可以看到相关职位都已经列出来,搜索框下面有两个下拉选择,分别是地区选择,和职位来源选择。不选择则返回所有地区和所有来源网站的职位信息。点击地区选择时:

    20201010013749690

    这里可以切换地区,选中地区确认后,则只筛选指定地区的职位信息。这里可以看到左侧有个“定位”按钮,定位时,将根据 wx.getLocation API 获取用户当前经纬度,然后调用 scf 云函数返回用户城市。达到定位效果。重置按钮,则重置筛选条件,将城市设置为不限制。

    20201010013758291

    来源列表里这里支持“猎聘网”、“智联招聘”、“前程无忧”、“Boos直聘”这4个来源。默认不限来源。

    20201010013807214

    进入详情界面,可以看到最下方有 “分享”、“收藏”、“申请职位” 三个功能按钮。

    点击分享,可以分享职位信息给朋友。

    收藏按钮,可以收藏在我的收藏列表里,在“我的收藏”界面里展示。

    申请职位按钮可以通过跳转别的网站小程序来实现,但是因为需要商务方面的工作,所以暂时没有实现申请职位。将来有机会,这个功能会实现的。

    此详情页面设置了可以分享到朋友圈,点击右上角的 ”…“ 就可以分享到朋友圈。

    20201010013833849

    “我的”界面里主要有收藏列表功能,用户首次使用时,头像那里有个“立即登录”,登录后,数据将通过上方所示云函数保存起来。

    20201010013839531

    这里是我的收藏界面。

    关于界面介绍写的很简单。不过这个小程序却不简单。能实现快速的筛选实时岗位信息,想要找工作的你,是否需要?

    运营网站

    运营网站首页

    20201010013849569

    用户访问统计

    20201010013850394

    申请职位统计

    20201010013852253

    搜索关键词排行榜Top10

    20201010013855697

    按城市搜索排行榜Top10

    20201010013858593

    功能代码展示

    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/

    体验小程序

    小程序的体验二维码:

    20201010013903366

    运营网站: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。

    【小程序源码网资源版权风险说明】:
    本站为避免不必要的纷争,分享的所有资源中一切可能有版权风险的资源将全部转载自第三方网站或平台,站长只为大家提供相关资源的介绍和跳转引导。 因可能有疏忽大意,所以如有遗漏资源侵犯了您的合法权利,请联系站长删除。
    【小程序源码网资源下载使用说明】:
    本站所分享的一切QQ小程序源码,thinkphp整站源码,微信小程序源码,图文教程等资源仅供用户学习参考使用,任何人不得作其他用途,违者自行承担所有责任。
    【小程序源码网毫无人看的介绍】:
    本站又称Z站,原名贼娘网,开站于2018年,换过三任站长,目前站长是第四任站长,本站是一个主要分享免费开源小程序源码/网站源码/免费素材/教程资源的网站,主要小程序资源有用于学习的小程序源码,也有正版原创可商用的小程序源码,是一个公益博客型网站。
    【小程序源码网原创源码版权申明】:
    未经小程序源码网许可,任何人不得擅自使用本站原创首发源码进行商业行为(除本站VIP用户在期限内,版权无使用限制),否则将依法承担相应赔偿责任。
    【小程序源码网转载文章版权申明】:
    本站所转载的QQ小程序或微信小程序源码与其他资源仅供学习,任何人不得作其他用途,违者自行承担所有责任。
    【小程序源码网站长最后的屁话】:
    如有您认为本站有任何侵犯您合法权益的文章,或者您有什么疑问需求,欢迎联系站长QQ,站长24小时在线,备注公司名称和源码版权问题或者需要小程序定制开发等站长业务类型可急速处理,如果您只是交流小程序的一些开发问题或源码问题可以加入QQ群讨论,就不用加站长啦,对于白嫖党,QQ群才是处理问题的天堂,当然站长也欢迎大家骚扰~
    小程序源码网 » 快速找工作微信小程序源码下载

    发表评论

    嘿,投喂下嘛!