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

发布于 5 年前作者 zlai1324 次浏览最后编辑 5 年前来自 share

项目概要


应用场景:

  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"

...

10 回复
mingyan
mingyan1 楼5 年前

非常棒滴

fengjie
fengjie2 楼5 年前

666

tao64
tao643 楼5 年前

棒棒的

ming30
ming304 楼5 年前

赞赞赞

kangwei
kangwei5 楼5 年前

厉害

rli
rli6 楼5 年前

超级赞

wuxia
wuxia7 楼5 年前

加油!

yqiu
yqiu8 楼5 年前

666

nhuang
nhuang9 楼5 年前

666

yaoyong
yaoyong10 楼11 个月前

劲抽