How to develop WeChat small program
preamble
Because of the recent indulgence and friends to go to mahjong from work, they recommended a small program for scoring, so you do not need to transfer money or record with playing cards in every game, but this small program not only opens with ads, the various pages are implanted with ads, and it is very uncomfortable to use.
So I made up my mind to jack a small program without ads. A week later, this little program was released.
You are welcome to visit and use my small program! Name of the applet:MahjongScorer
reasoning
1. Register and get an avatar and nickname. Registered users are automatically logged in directly.
2. Create a room, sweep to join the room or forward WeChat friends, group chat, by clicking to join.
3. Add a table board to keep track of the table fees drawn in each game (no table fees are required and can be ignored).
4. At the end of each game, record everyone's wins and losses.
5. After dispersing the room, save all records of this game to your personal history and dismiss the room.
6. Evaluation, random pop-up evaluation page to evaluate the experience.
7. Feature: To facilitate the recording of step 2, add long-press voice recognition.
preliminary
forward part of sth.
Tools: HBuilderX, WeChat Developer Tools
Framework: uni-app (Vue3), pinia
Because of the simplicity of the page, no UI framework was used.
The basic project structure of uni-app is attached:
┌─uniCloud cloud space directory, uniCloud-aliyun for AliCloud, uniCloud-tcb for Tencent Cloud (see uniCloud for details)
│ ─components Directory of vue-compliant uni-app components.
│ └─Reusable a-components
├─hybrid App directory for local html files.
├─platforms Directory for storing platform-specific pages.
│ ├─pages Directory for business page files
│ ├─index
│ │ └─ index pages
│ └─list
│ └─ list pages
├─static The directory where local static resources (such as images, videos, etc.) referenced by the application are stored, note: static resources can only be stored here.
├─uni_modules A directory for plug-ins of the uni_modules specification.
├─wxcomponents Directory for applet components.
├─ Vue initialization entry file
├─ App configuration, used to configure the global style of the app and listen to the app lifecycle.
├─ Configure packaging information such as app name, appid, logo, version, and so on.
└─ Configure page class information such as page routing, navigation bar, tabs, and so on.
1. New projects
Create a new project in HBuilderX.
2. Configure the development tool path
Configure the installation path of WeChat Developer Tool in HBuilderX: Tools - Settings - Running Configuration - Small Program Running Configuration - WeChat Developer Tool Path.
3. Open the port
In WeChat Developer Tools, open the port: Settings - Security Settings - Service Port.
4. Running the project
Run the project in HBuilderX: Run - Run to Applet Emulator - WeChat Developer Tools.
This will automatically open the WeChat Developer Tools according to the path configured in step 2.
At this point, the front-end basics are ready. If you want to write a small program with only front-end static pages without the need for back-end services, then you can not continue to read further, only:
- Writing code for static pages
- Click Login in the top left corner of WeChat Developer Tools, click Details-Basic Information-AppId in the top right corner, click Modify to your registered applet ID and click Upload.
- The AppID is obtained by going toWeChat small program official websiteIf you are a developer, find Development - Development Management - Development Settings, under "Developer ID", you can get the AppID (applet ID).
- Find Management-Version Management-Development Version, click Submit for Review, and then release the reviewed version after it is reviewed and approved to complete the development and release of the small program.
back end
Tool: IDEA
Framework: SpringBoot
Database: MySQL
1. Groundwork
Refer to the previous blog:How to build your own websitecap (a poem)How to build your own website (II)Perform server setup and jar package deployment.
2. Configure the server domain name
go intoWeChat small program official websiteIf you are a developer, find Development-Development Management-Development Settings, under "Developer ID", get the AppID (applet ID), AppSecret (applet key) and save it, which will be used for subsequent interfaces. Continue to slide down to "Server Domain", fill in the server domain name in request legal domain name, uploadFile legal domain name, downloadFile legal domain name, that is, the server domain name deployed in the first step of the basic work.
Write code
forward part of sth.
Some of the key code is pasted below.
State Management Library
Since there are a lot of common global properties and methods, this section is placed in pinia's global state management library.
::: details Click to view code
import { defineStore } from "pinia".
const useUserStore = defineStore("useUserStore", {
state: () => {
return {
info: {
// User information
openid: "",
avatar: "",
avatar: "", nickname: "", roomid: "",
roomid: "",
}, isLogin: null, // Whether or not to log in.
isLogin: null, // Whether or not you are logged in, used to determine if the login page is displayed.
shareid: "", // The id of the room that was shared via QR code or clicking on it
members: [], // Members of the room
records: [], // All games played in the current game.
circle: 1, // Number of games
sumArr: [], // The total score of the game so far
timer: null, // timer, triggered 2 seconds after room creation, listens for room member changes.
scene: null, // Scene: distinguish if the room was entered through a friend circle, if you can't get the openid when you enter through a friend circle you will get an error
qrCode: "", // The room's QR code.
baseURL: "/mahjong/", // back-end interface prefix, to save each call to the interface to write a large number of prefixes
}
}
actions: {
async updateInfo(data, mode) {}, // add user, change avatar/nickname
async getOpenid() {}, // get the user's openid
async autoLogin(roomid) {}, // If the user's openid is in the cache, log in directly, otherwise get the user's openid and store it in the cache.
async getRecords() {}, // Get all games in this game.
async getMembers() {}, // Get the members of the room.
getSum() {}, // Get the total score of the current game.
async updateRoomid(roomid) {}, // Update the room id of the current user.
async gameOver() {}, // End the game and dismiss the room.
setTimer() {}, // Set the timer to get member changes.
}).
:::
2. Registration
When registering, you can click on the avatar to choose the avatar, do not choose is the default head click on the nickname to enter the nickname, the nickname can not be empty, and up to eight words.
::: details Click to view code
<view class="container">
<view class="avatarUrl">
<button
type="balanced"
open-type="chooseAvatar"
@chooseavatar="onChooseavatar"
>
<image
:src="avatarUrl"
class="refreshIcon"
v-if="avatarUrl !== defaultAvatar"
></image>
<image v-else src="/static/" class="upload"></image>
</button>
</view>
<view class="nickname">
<input
maxlength="8"
type="nickname"
:value="nickName"
@blur="bindblur"
placeholder="Click to enter a nickname"
@input="bindinput"
/>
</view>
<view class="operation">
<button class="confirm" @click="onSubmit">save (a file etc) (computing)</button>
<button v-if="" class="cancel" @click="cancel">
abolish
</button>
</view>
</view>
:::
3.Home
Enter the home page, click on your avatar to change your avatar and nickname, click on the plus sign to share the room with your contacts or group chat, and other users can join the room by clicking share.
Long press the plus sign to generate a station board for recording station charges, and click the board to delete it.
Click Scan to Join to generate the current room applet code, other users can join the room by scanning the applet code.
Click Start to go to the scoring panel. Clicking Finish will end the game and dismiss the current room, and the score of the game will be saved to the "Record" menu.
::: details Click to view code
async generateCode() { // Generate the current room applet code
= true
()
let roomid = ''
if () {
roomid =
} else {
roomid = md5( + new Date().getTime())
(roomid)
}
let res = await ({
url: + `get-code?roomid=${}`,
method: 'post',
responseType: 'arraybuffer',
})
= 'data:image/PNG;BASE64,' + uni.arrayBufferToBase64()
()
= false
}
:::
4. Scoring
Select win/loss in the win/loss column and enter each member's score in the score column. It is also possible to enter a positive/negative number in the score field without selecting a win/loss. The score of the last member is not required, it will be calculated automatically according to the rule that the sum of all members' scores is 0.
Press and hold the OK button to recognize the voice, the result will be automatically filled in the score column. The voice template is: Nickname/nth + Loss/Win/Gain/Subtract/Positive/Victory/Loss+How many.
::: details Click to view code
startRecording() {
if () return;
= true;
({
duration: 60000, // recording duration,In milliseconds
format: "mp3", // recording format
});
= "Speaking.…";
},
stopRecording() {
if (!) return;
= false;
();
},
async handleRecordingStop(res) {
let base64code = uni
.getFileSystemManager()
.readFileSync(, "base64");
();
let rst = await ({
url: + "/translate/voice",
method: "post",
data: {
data: base64code,
customizationId: "xxxxxxxxxxxxxxxxxxxxx",
},
header: {
"content-type": "application/x-www-form-urlencoded",
},
});
();
if () {
= ;
();
} else {
= "";
({
title: "I don't think I heard anything.~",
icon: "none",
});
}
},
words2Number(words) { // Converting sentences to numbers
const one2ten = [
""one" radical in Chinese characters (Kangxi radical 1)",
"stupid (Beijing dialect)",
"surname San",
"4",
"5",
"6",
"7",
"8",
"9",
"10",
"a few",
];
const wordArr = ("");
let number = null;
for (let i = 1; i < 12; i++) {
if ((one2ten[i - 1])) {
number = i < 11 ? i : 2;
break;
}
}
return number;
},
processingData() {
// Define words that mean win and lose and their corresponding plus and minus signs
const winKeywords = ["beat", "plus", "greater than zero", "superb (of vista)"];
const loseKeywords = ["enter (a password)", "diminish", "turn one's back on"];
const indexs = ["第"one" radical in Chinese characters (Kangxi radical 1)个", "第stupid (Beijing dialect)个", "第surname San个", "第4个", "第5个"];
// Initialize the result object
const result = {};
// Split text by comma
const phrases = (",");
// Iterate over each phrase
((phrase) => {
let nickname = "";
let score = 0;
// 判断短语中是否包含beat和enter (a password)的意思的词
const winKeyword = ((keyword) =>
(keyword)
);
if (winKeyword) {
// Extract nicknames and scores
const data = (winKeyword);
if ( === 2) {
nickname = data[0];
((x, i) => {
if ((x))
nickname = [i].nickname;
});
const matchScore = data[1].match(/\d+/g);
if (matchScore?.length) {
score = 1 * matchScore[0];
} else {
score = this.words2Number(data[1]);
}
}
} else {
const loseKeyword = ((keyword) =>
(keyword)
);
if (loseKeyword) {
// Extract nicknames and scores
const data = (loseKeyword);
if ( === 2) {
nickname = data[0];
((x, i) => {
if ((x))
nickname = [i].nickname;
});
const matchScore = data[1].match(/\d+/g);
if (matchScore?.length) {
score = -1 * matchScore[0];
} else {
score = -1 * this.words2Number(data[1]);
}
}
}
}
// 添plus到结果对象中
if (nickname && score) result[nickname] = score;
});
if ((result).length === 0) {
({
title: "I didn't hear you.~",
icon: "none",
});
} else {
for (let key in result) {
((x, i) => {
if (() || (key)) {
[i] = (result[key]);
[i] = result[key] >= 0 ? "+" : "-";
}
});
}
(); // 自动填写最后"one" radical in Chinese characters (Kangxi radical 1)个成员的得分
}
}
:::
5. Records
Click on the "Records" menu to see all your games in history.
Click on a matchup to view the details of that matchup.
6. Details
Click on an avatar to display a nickname.
::: details Click to view code
async showUser(record, index) {
let nickname = ''
let openid = (',')[index]
let findUser = ((x) => === openid)
if (findUser) {
nickname =
} else {
let res = await ({
url: + `search-info`,
method: 'get',
data: { openid },
})
nickname = [0].nickname
({ openid, nickname }) // The interface is not called again if it has already been ordered
()
}
({
title: nickname,
icon: 'none',
})
}
:::
7. Evaluation
First of all, introduce the "Evaluation Publishing Component" in it.
"plugins": {
"wxacommentplugin": {
"version": "latest",
"provider": "wx82e6ae1175f264fa"
}
}
If you haven't added a plug-in yet, click "Add Plug-in" in the Developer Tools Console.
The component interface can then be called from within the page's js file.
var plugin = requirePlugin("wxacommentplugin");
({
success: (res) => {
(" success", res);
},
fail: (res) => {
(" fail", res);
},
});
available atWeChat small program official websiteCheck out the reviews in the Feature-Experience Reviews.
back end
1. Build a table
Two tables are used, a user table mahjong and a game table room.
id: user id
openid: the user's openid
avatar: the user's avatar
nickname: the user's nickname
roomid: the room the user is currently in
updateTime: the time the user joined the room
id: game id
roomid: room id
openids: openids of all members of the game
scores: the scores of the game
circle: number of games
createTime: the end time of the game
active: if or not the game is over
2. Registration
① The front-end obtains the code through this request, and calls WeChat's official interface by combining the code (js_code) with the appid and secret of the applet and grant_type=authorization_code."/sns/jscode2session", which returns the user's openid.
::: details Click to view code
public JSONObject getOpenid(String code) throws Exception {
String appid = "xxx";
String secret = "xxx";
("code=" + code);
HttpClient httpClient = ();
URI url = new URIBuilder("/sns/jscode2session")
.setParameter("appid", appid)
.setParameter("secret", secret)
.setParameter("js_code", code)
.setParameter("grant_type", "authorization_code")
.build();
HttpGet httpGet = new HttpGet(url);
JSONObject json = new JSONObject();
try {
HttpResponse res = (httpGet);
if (().getStatusCode() == HttpStatus.SC_OK) {
String result = (());// come (or go) backjsonspecification:
json = (result);
} else {
throw new Exception("gainopenidfail (e.g. experiments)!");
}
} catch (Exception e) {
throw new Exception("gainopenidexceptions!");
}
return json;
}
:::
② After the user fills in the avatar and nickname, the front-end sends a request, and the back-end stores the received avatar in minio and returns the avatar path, and then combines the information such as openid, nickname, room id, etc. and stores it in the mahjong table.
3. Generate the current room applet code
First call the official WeChat interface to get a token via appid, secre, grant_type=client_credential, and then use this token in combination with the current room id passed by the front-end to generate the current room applet code.
::: details Click to view code
public byte[] getCode(String roomid) throws Exception {
String appid = "xxx";
String secret = "xxx";
HttpClient httpClient = ();
URI tokenURI = new URIBuilder("/cgi-bin/token")
.setParameter("appid", appid)
.setParameter("secret", secret)
.setParameter("grant_type", "client_credential")
.build();
HttpGet httpGet = new HttpGet(tokenURI);
JSONObject json = new JSONObject();
try {
HttpResponse res = (httpGet);
if (().getStatusCode() == HttpStatus.SC_OK) {
json = ((()));
URI codeURI = new URIBuilder("/wxa/getwxacode")
.setParameter("access_token", ("access_token"))
.build();
HttpPost httpPost = new HttpPost(codeURI);
String body = "{\"path\": \"pages/home/index?room\"}";
(new StringEntity(body));
try {
HttpResponse rst = (httpPost);
if (().getStatusCode() == HttpStatus.SC_OK) {
return (());
} else {
throw new Exception("Failed to get applet code!");
}
} catch (Exception e) {
throw new Exception("Get Applet Code Exception!");
}
} else {
throw new Exception("gaintokenfail (e.g. experiments)!");
}
} catch (Exception e) {
throw new Exception("gaintokenexceptions!");
}
}
:::
4. Speech Recognition
It uses Tencent Cloud's sentence recognition, which is free for 5000 times per month. The front-end converts the recording temporary file into base64 encoding and passes it to the back-end, combining the usage scenario EngSerViceType, the source of speech data SourceType (0: speech URL; 1: speech data), the speech Url, the audio format VoiceFormat, and the timestamps, hot words, and self-learning models.
::: details Click to view code
public JSONObject voiceTrans(@RequestParam(required = false) String engSerViceType, @RequestParam(required = false) Long sourceType,
@RequestParam(required = false) String url, @RequestParam(required = false) String voiceFormat,
@RequestParam(required = false) String data, @RequestParam(required = false) String customizationId ) {
if (engSerViceType == null) engSerViceType = "16k_zh";
if (sourceType == null) {
if (url == null) sourceType = 1L;
else sourceType = 0L;
}
if (voiceFormat == null) voiceFormat = "mp3";
JSONObject json = new JSONObject();
String result = (engSerViceType, sourceType, url, voiceFormat, data, customizationId);
json = (result);
return json;
}
public static String voiceTrans(String EngSerViceType, Long SourceType, String Url, String VoiceFormat, String Data, String CustomizationId ) {
try {
// The key can be found on the official console /cam/capi carry out acquisition
Credential cred = new Credential("xxx", "xxx");
// Instantiate ahttpoptions (as in computer software settings),optional,No special needs can be skipped
HttpProfile httpProfile = new HttpProfile();
("");
// Instantiate aclientoptions (as in computer software settings),optional,No special needs can be skipped
ClientProfile clientProfile = new ClientProfile();
(httpProfile);
// Instantiation to request the product'sclientboyfriend,clientProfile是optional
AsrClient client = new AsrClient(cred, "", clientProfile);
// Instantiate a请求boyfriend,Each interface will correspond to arequestboyfriend
SentenceRecognitionRequest req = new SentenceRecognitionRequest();
(EngSerViceType);
(SourceType);
(Url);
(VoiceFormat);
(Data);
(1L);
(1L);
(CustomizationId);
// returnedrespanSentenceRecognitionResponsepractical example,与请求boyfriend对应
SentenceRecognitionResponse resp = (req);
// exportsjsonFormatting strings for packet return
return (resp);
} catch (TencentCloudSDKException e) {
return ();
}
}
:::
5. Add records
When a game is over, store the room id, all members openid, score, and number of games in the room table, and set active to 1.
First query the mahjong table to see if the room id exists, if not, the game is over and the room has been disbanded. Then query the room table to see if there is a game with this room id and active equal to 1: ① If there is no such room id and circle is equal to 1, then judge the number of members in the room and the number of members submitted, if the former is greater than the latter, it means that there are new members joining the room, and the submission will not take effect. If the former is greater than the latter, it means there are new members joining and the submission is not valid. ② If it doesn't exist and circle is not equal to 1, it means that it's not the first game and the room is already locked, so other people can't join in, so there is no need to judge and the submission is valid. ③ If it exists and the number of games in the last game record of the room is less than the number of games submitted, then the submission is valid. ④ If it exists and the number of games in the last match record of the room is greater than or equal to the number of games submitted, it means that someone has already submitted the game before you submit it, so the submission is invalid.
::: details Click to view code
public String addRecord(Room record) {
String openids = ();
Integer circle = ();
List<Room> records = (openids);
List<Mahjong> mahjong = ("", ());
if (()) {
return "The game is over.";
} else {
if (()) {
if (circle == 1) {
if (() > ().split(",").length) {
return "We have a new member.";
}
}
(record);
return "true";
} else {
if ((() - 1).getCircle() < circle) {
(record);
return "true";
} else {
return "Submitted by other members";
}
}
}
}
:::
6. End game
The front-end passes the openid of all the members of this game, the ids of all the games, the total score, and the room id to the back-end, which does the following in order:
① Iterate over the openids by (openids) and clear the user room ids in the mahjong table whose openid is equal to it.
② Iterate through all the pair ids by (ids) and set the active to 0 for the pair whose id is equal to that in the room table.
③ Add the total score as a match record to the room table by (record), where circle equals -1 and active equals 0.
::: details Click to view code
public void gameOver(String[] openids, String[] ids, String scores, String roomid) {
(openids);
(ids);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
(("Asia/Shanghai"));
String createTime = (new Date());
Room record = new Room(null, (",", ids), (",", openids), scores, -1, createTime, 0);
(record);
}
:::
7. Query history
This interface logic is written a bit haphazardly and can be optimized subsequently.
First query the room table by individual openid for the history records related to it with circle equal to -1.
Then query the mahjong table for all user information, traverse the history, traverse the user information, and
Extracts the user's avatar whose openid is equal to the openid of the user's information from the history, and returns it together with the history.
::: details Click to view code
public List searchHistory(String openid) {
List<Room> records = (openid);
List<Mahjong> mahjongs = ();
for (Room record : records) {
String[] openids = ().split(",");
String avatars = "";
String avatar = "";
for (String id : openids) {
avatar = "";
for (Mahjong mahjong: mahjongs)
{
if(().equals(id)){
avatar = ();
break;
}
}
avatars += avatar + ',';
}
((0, () - 1));
}
return records;
}
:::
ultimate
If you are interested, you can chat with me privately to get the detailed code.