微信小程序开发到发布完整教程

微信小程序开发到发布完整教程

17 2025-09-07

目录

1. [项目概述]

2. [开发环境准备]

3. [项目结构搭建]

4. [后端服务开发]

5. [服务器部署]

6. [小程序开发]

7. [域名配置]

8. [测试与调试]

9. [发布上线]

10. [后续优化]

---

1. 项目概述

1-1.项目名称

一土致知学校家长接送签到系统

1-2. 功能描述

  • 家长注册学生信息

  • 学生ID验证(与飞书文档集成)

  • 学生签到管理

  • 考勤记录查看

1-3. 技术栈

  • 前端:微信小程序

  • 后端:Node.js + Express

  • 数据库:模拟数据(可扩展为真实数据库)

  • 服务器:阿里云ECS

  • 域名zrjm.cc

---

2. 开发环境准备

2-1. 微信开发者工具

  1. 下载地址:https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html

  2. 安装并登录微信开发者账号

  3. 创建小程序项目

2-2. Node.js环境

  1. 下载地址:https://nodejs.org/

  2. 安装Node.js(推荐LTS版本)

  3. 验证安装:`node --version`

2-3. 代码编辑器

推荐使用VSCode或WebStorm

---

3.项目结构搭建

3-1. 创建项目目录

F:\Etu\

├── app.js # 小程序主文件

├── app.json # 小程序配置

├── app.wxss # 全局样式

├── images/ # 图片资源

│ ├── school_logo.png

│ └── school_logo2.png

├── pages/ # 页面文件

│ ├── login/ # 登录页面

│ │ ├── login.js

│ │ ├── login.wxml

│ │ └── login.wxss

│ ├── register/ # 注册页面

│ │ ├── register.js

│ │ ├── register.wxml

│ │ └── register.wxss

│ └── checkin/ # 签到页面

│ ├── checkin.js

│ ├── checkin.wxml

│ └── checkin.wxss

├── utils/ # 工具文件

│ └── api.js # API接口封装

├── server/ # 后端服务

│ ├── app.js

│ ├── package.json

│ └── services/

└── deploy/ # 部署配置

├── docker/

└── nginx/

3-2. 初始化小程序项目

在微信开发者工具中:

1. 选择"小程序"

2. 填写项目信息:

- 项目名称:家长接送签到系统

- 目录:选择F:\Etu

- AppID:填写您的小程序AppID

3. 点击"新建"

---

4. 后端服务开发

4-1. 创建后端项目

4-1-1 初始化package.json

cd server
npm init -y

4-1-2 安装依赖

npm install express cors axios dotenv body-parser

4-1-3 创建主文件 app.js

const express = require('express');

const cors = require('cors');

const bodyParser = require('body-parser');

const app = express();

const PORT = process.env.PORT || 3000;

// 中间件

app.use(cors());

app.use(bodyParser.json());

app.use(bodyParser.urlencoded({ extended: true }));

// 模拟学生数据

const mockStudents = [

{

studentId: '10001',

studentName: '陈奕宏',

parentName: '陈柯辰',

relation: '父亲',

grade: 'G5',

className: 'G5-1凤凰班',

homeroomTeacher: '汤萍'

},

{

studentId: '10002',

studentName: '陈奕欣',

parentName: '陈柯辰',

relation: '父亲',

grade: 'G3',

className: 'G3-1天马班',

homeroomTeacher: '韦丽平'

}

];

// API路由

app.get('/api/students', (req, res) => {

res.json({

success: true,

data: mockStudents

});

});

app.get('/api/students/:studentId', (req, res) => {

const { studentId } = req.params;

const student = mockStudents.find(s => s.studentId === studentId);

if (student) {

res.json({

success: true,

data: student

});

} else {

res.status(404).json({

success: false,

message: '未找到该学生'

});

}

});

app.post('/api/students/verify', (req, res) => {

const { studentId } = req.body;

const student = mockStudents.find(s => s.studentId === studentId);

res.json({

success: true,

exists: !!student,

data: student || null

});

});

app.get('/health', (req, res) => {

res.json({

success: true,

message: '服务运行正常',

timestamp: new Date().toISOString()

});

});

// 启动服务器

app.listen(PORT, '0.0.0.0', () => {

console.log(`服务器运行在端口 ${PORT}`);

console.log(`健康检查: http://localhost:${PORT}/health`);

});

module.exports = app;

4-2. 测试后端服务

启动服务

node app.js

测试接口

curl http://localhost:3000/health
curl http://localhost:3000/api/students

---

5. 服务器部署

5-1. 购买阿里云服务器

5-1-1 选择配置

- CPU:2核

- 内存:4GB

- 系统:CentOS 7.x 或 Ubuntu 18.04+

- 带宽:3Mbps

5-1-2 配置安全组

开放端口:

- 22 (SSH)

- 80 (HTTP)

- 443 (HTTPS)

- 3000 (后端服务)

5-2. 域名配置

5-2-1 购买域名

- 在阿里云购买域名:zrjm.cc

- 完成域名实名认证

5-2-2 域名解析

添加A记录:

- 主机记录:@

- 记录类型:A

- 记录值:服务器公网IP

- TTL:600

5-3. 服务器环境配置

5-3-1 连接服务器

ssh root@your-server-ip

5-3-2 更新系统

#CentOS

yum update -y

#Ubuntu

apt update && apt upgrade -y

5-3-3 安装Node.js

# 使用NodeSource仓库

curl -fsSL https://rpm.nodesource.com/setup_18.x | bash -
yum install -y nodejs

# 验证安装

node --version
npm --version

```

5-3-4 安装Nginx

# CentOS

yum install -y nginx

# Ubuntu

apt install -y nginx

# 启动服务

systemctl start nginx
systemctl enable nginx

5-3-5 安装PM2

npm install -g pm2

5-4. 配置SSL证书

5-4-1 安装certbot

# CentOS

yum install -y certbot python2-certbot-nginx

# Ubuntu

apt install -y certbot python3-certbot-nginx

5-4-2 申请证书

certbot --nginx -d zrjm.cc

5-4-3 验证证书

测试HTTPS

curl https://zrjm.cc

5-5. 部署后端服务

5-5-1 上传代码

# 创建项目目录

mkdir -p /opt/etu-miniprogram
cd /opt/etu-miniprogram

# 上传代码文件(使用scp或git)

scp -r server/* root@your-server-ip:/opt/etu-miniprogram/

5-5-2 安装依赖

cd /opt/etu-miniprogram
npm install

5-5-3 启动服务

# 使用PM2启动

pm2 start app.js --name "etu-backend"

# 设置开机自启

pm2 startup
pm2 save

# 查看状态

pm2 status

5-5-4 配置Nginx反向代理

# 备份原配置

cp /etc/nginx/sites-enabled/default /etc/nginx/sites-enabled/default.backup

# 创建新配置

cat > /etc/nginx/sites-enabled/default << 'EOF'
server {
    listen 80;
    server_name zrjm.cc;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name zrjm.cc;
    
    # SSL证书配置
    ssl_certificate /etc/letsencrypt/live/zrjm.cc/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/zrjm.cc/privkey.pem;
    
    # SSL安全配置
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
    
    # 反向代理到后端服务
    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
    }
}
EOF

# 重启Nginx

nginx -t
systemctl restart nginx

5-6. 测试部署结果

# 测试本地连接

curl http://localhost:3000/health

# 测试HTTPS连接

curl https://zrjm.cc/health
curl https://zrjm.cc/api/students

---

6. 小程序开发

6-1. 创建页面文件

6-1-1 登录页面 (pages/login/)

login.wxml

xml

<view class="container">

<view class="school-info">

<image class="school-logo" src="/images/school_logo2.png" mode="aspectFit"></image>

</view>

<view class="login-form">

<view class="form-title">家长接送签到系统</view>

<button class="login-btn" bindtap="goToRegister">开始使用</button>

</view>

</view>

login.wxss

css

.container {

padding: 30rpx;

background-color: var(--light-green);

min-height: 100vh;

}

.school-info {

display: flex;

flex-direction: column;

align-items: center;

margin-bottom: 100rpx;

}

.school-logo {

width: 300rpx;

height: 150rpx;

}

.login-form {

text-align: center;

}

.form-title {

font-size: 36rpx;

font-weight: 600;

color: var(--primary-color);

margin-bottom: 80rpx;

}

.login-btn {

background: var(--gradient-green);

color: white;

border-radius: 40rpx;

height: 90rpx;

line-height: 90rpx;

font-size: 32rpx;

width: 100%;

}

login.js

javascript

Page({

goToRegister: function() {

wx.navigateTo({

url: '/pages/register/register'

})

}

})

6-1-2 注册页面 (pages/register/)

register.wxml

xml

<view class="container">

<view class="school-info">

<image class="school-logo" src="/images/school_logo2.png" mode="aspectFit"></image>

</view>

<view class="form-title">添加学生信息</view>

<form bindsubmit="submitForm">

<view class="form-group">

<view class="form-label">家长姓名</view>

<input class="form-input" name="parentName" placeholder="请输入家长姓名" value="{{parentName}}"/>

</view>

<view class="form-group">

<view class="form-label">与学生关系</view>

<picker bindchange="relationChange" value="{{relationIndex}}" range="{{relations}}">

<view class="picker">

{{relations[relationIndex] || '请选择与学生关系'}}

</view>

</picker>

</view>

<view class="form-group">

<view class="form-label">学生姓名</view>

<input class="form-input" name="studentName" placeholder="请输入学生姓名" value="{{studentName}}"/>

</view>

<view class="form-group">

<view class="form-label">学生ID</view>

<input class="form-input" name="studentId" placeholder="请输入学生ID" value="{{studentId}}"/>

</view>

<button class="submit-btn" type="primary" form-type="submit" disabled="{{loading}}">

{{loading ? '验证中...' : '提交'}}

</button>

</form>

</view>

register.wxss

css

.container {

padding: 30rpx;

background-color: var(--light-green);

}

.form-title {

font-size: 36rpx;

font-weight: 600;

text-align: center;

margin: 30rpx 0 50rpx;

color: var(--primary-color);

}

.form-group {

margin-bottom: 30rpx;

background-color: rgba(255, 255, 255, 0.8);

padding: 20rpx;

border-radius: 12rpx;

box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);

}

.form-label {

font-size: 28rpx;

color: var(--text-color);

margin-bottom: 10rpx;

font-weight: 500;

}

.form-input {

border: 1rpx solid rgba(0, 0, 0, 0.1);

border-radius: 8rpx;

padding: 15rpx;

font-size: 28rpx;

background-color: rgba(255, 255, 255, 0.9);

}

.picker {

border: 1rpx solid rgba(0, 0, 0, 0.1);

border-radius: 8rpx;

padding: 15rpx;

font-size: 28rpx;

color: #666;

background-color: rgba(255, 255, 255, 0.9);

}

.submit-btn {

margin-top: 50rpx;

border-radius: 40rpx;

background: var(--gradient-green) !important;

height: 90rpx;

line-height: 90rpx;

font-size: 32rpx;

letter-spacing: 2rpx;

box-shadow: 0 8rpx 16rpx rgba(94, 176, 69, 0.3);

border: none;

}

.school-info {

display: flex;

flex-direction: column;

align-items: center;

margin-bottom: 30rpx;

}

.school-logo {

width: 300rpx;

height: 150rpx;

}

register.js

javascript

const api = require('../../utils/api');

Page({

data: {

parentName: '',

relations: ['父亲', '母亲', '其他'],

relationIndex: 0,

studentName: '',

studentId: '',

loading: false

},

onLoad: function (options) {

const app = getApp()

if (app.globalData.studentInfo) {

const studentInfo = app.globalData.studentInfo

this.setData({

parentName: studentInfo.parentName || '',

relationIndex: studentInfo.relationIndex || 0,

studentName: studentInfo.studentName || '',

studentId: studentInfo.studentId || ''

})

}

},

relationChange: function(e) {

this.setData({

relationIndex: e.detail.value

})

},

// 验证学生ID和姓名是否匹配

verifyStudentInfo: function(studentId, studentName) {

var that = this

return api.verifyStudentId(studentId).then(function(response) {

if (response.success && response.exists) {

var feishuStudent = response.data

// 比较学生姓名是否匹配(忽略空格和大小写)

var inputName = studentName.trim().replace(/\s+/g, '')

var feishuName = feishuStudent.studentName.trim().replace(/\s+/g, '')

if (inputName === feishuName) {

return {

success: true,

student: feishuStudent

}

} else {

return {

success: false,

message: '学生姓名与学生ID不匹配'

}

}

} else {

return {

success: false,

message: '学生ID不存在'

}

}

}).catch(function(error) {

console.error('验证学生信息失败:', error)

return {

success: false,

message: '验证失败,请检查网络连接'

}

})

},

submitForm: function (e) {

var parentName = e.detail.value.parentName

var studentName = e.detail.value.studentName

var studentId = e.detail.value.studentId

var relation = this.data.relations[this.data.relationIndex]

console.log('提交表单:', { parentName, studentName, studentId, relation })

if (!parentName || !relation || !studentName || !studentId) {

wx.showToast({

title: '请填写所有信息',

icon: 'none'

})

return

}

// 设置加载状态

this.setData({ loading: true })

var that = this

// 由于后端API可能还未部署,先使用模拟验证

this.simulateVerification(studentId, studentName).then(function(verifyResult) {

that.setData({ loading: false })

console.log('验证结果:', verifyResult)

if (verifyResult.success) {

// 验证成功,添加学生

var newStudent = {

parentName: parentName,

relation: relation,

studentName: studentName,

studentId: studentId,

grade: verifyResult.student.grade || '',

className: verifyResult.student.className || '',

homeroomTeacher: verifyResult.student.homeroomTeacher || ''

}

// 获取现有的学生列表

var app = getApp()

var existingStudentList = app.globalData.studentList || []

// 检查学生ID是否已存在

var isDuplicate = false

for (var i = 0; i < existingStudentList.length; i++) {

if (existingStudentList[i].studentId === studentId) {

isDuplicate = true

break

}

}

if (isDuplicate) {

wx.showToast({

title: '该学生已添加',

icon: 'none'

})

return

}

// 添加新学生到列表

existingStudentList.push(newStudent)

// 更新全局数据

app.globalData.studentList = existingStudentList

app.globalData.studentInfo = newStudent

// 保存到本地存储

wx.setStorage({

key: 'studentList',

data: existingStudentList,

success: function() {

wx.setStorage({

key: 'studentInfo',

data: newStudent,

success: function() {

wx.showToast({

title: '添加成功',

icon: 'success',

duration: 1500,

success: function() {

wx.redirectTo({

url: '/pages/checkin/checkin',

})

}

})

}

})

},

fail: function(err) {

console.error("保存失败", err)

wx.showToast({

title: '添加失败',

icon: 'none'

})

}

})

} else {

// 验证失败,显示错误信息

wx.showToast({

title: verifyResult.message || '添加失败,请核查学生姓名与学生ID是否有误',

icon: 'none',

duration: 3000

})

}

}).catch(function(error) {

that.setData({ loading: false })

console.error('验证过程出错:', error)

wx.showToast({

title: '验证失败,请检查网络连接',

icon: 'none',

duration: 3000

})

})

},

// 模拟验证函数(用于测试,实际部署后可以删除)

simulateVerification: function(studentId, studentName) {

var that = this

return new Promise(function(resolve) {

// 模拟网络延迟

setTimeout(function() {

// 模拟验证逻辑:如果学生ID是10001且姓名是陈奕宏,则验证通过

if (studentId === '10001' && studentName === '陈奕宏') {

resolve({

success: true,

student: {

studentId: '10001',

studentName: '陈奕宏',

grade: 'G5',

className: 'G5-1凤凰班',

homeroomTeacher: '汤萍'

}

})

} else if (studentId === '10002' && studentName === '陈奕欣') {

resolve({

success: true,

student: {

studentId: '10002',

studentName: '陈奕欣',

grade: 'G3',

className: 'G3-1天马班',

homeroomTeacher: '韦丽平'

}

})

} else {

resolve({

success: false,

message: '添加失败,请核查学生姓名与学生ID是否有误'

})

}

}, 1000)

})

}

})

6-1-3 签到页面 (pages/checkin/)

checkin.wxml

<view class="container">

<view class="header">

<view class="school-info">

<image class="school-logo" src="/images/school_logo2.png" mode="aspectFit"></image>

<view class="school-name">一土致知学校</view>

</view>

<view class="student-info">

<view class="student-avatar" bindtap="changeAvatar">

<image wx:if="{{currentStudent.avatar}}" src="{{currentStudent.avatar}}" mode="aspectFill"></image>

<view wx:else class="avatar-placeholder">头像</view>

</view>

<view class="student-details">

<view class="student-name">{{currentStudent.studentName}}</view>

<view class="student-id">学号:{{currentStudent.studentId}}</view>

<view class="parent-info">{{currentStudent.parentName}}({{currentStudent.relation}})</view>

</view>

</view>

</view>

<view class="today-info">

<view class="date">{{todayDate}}</view>

<view class="attendance-status">

<view class="status-item" wx:if="{{todayAttendance.entry}}">

<text class="status-label">入校:</text>

<text class="status-time">{{todayAttendance.entry}}</text>

</view>

<view class="status-item" wx:if="{{todayAttendance.exit}}">

<text class="status-label">出校:</text>

<text class="status-time">{{todayAttendance.exit}}</text>

</view>

</view>

</view>

<view class="student-list" wx:if="{{studentList.length > 1}}">

<view class="list-title">切换学生</view>

<view class="student-items">

<view

class="student-item {{currentStudentId === item.studentId ? 'active' : ''}}"

wx:for="{{studentList}}"

wx:key="studentId"

bindtap="switchStudent"

data-student-id="{{item.studentId}}"

>

{{item.studentName}}

</view>

</view>

</view>

<view class="actions">

<button class="action-btn add-student" bindtap="addStudent">添加学生</button>

<button class="action-btn logout" bindtap="testLogout">退出登录</button>

</view>

<view class="calendar-section">

<view class="calendar-header">

<button class="month-btn" bindtap="prevMonth">‹</button>

<view class="month-year">{{currentYear}}年{{currentMonth}}月</view>

<button class="month-btn" bindtap="nextMonth">›</button>

</view>

<view class="calendar-grid">

<view class="weekdays">

<view class="weekday" wx:for="{{['日','一','二','三','四','五','六']}}" wx:key="*this">{{item}}</view>

</view>

<view class="days">

<view

class="day {{item.isCurrentMonth ? 'current-month' : ''}} {{item.isToday ? 'today' : ''}} {{item.hasCheckin ? 'has-checkin' : ''}}"

wx:for="{{calendarDays}}"

wx:key="index"

bindtap="onDayTap"

data-day="{{item.day}}"

>

{{item.day}}

<view class="checkin-indicator" wx:if="{{item.hasCheckin}}">

<view class="indicator-dot {{item.checkinType}}"></view>

</view>

</view>

</view>

</view>

</view>

</view>

checkin.wxss

.container {

padding: 20rpx;

background-color: var(--light-green);

min-height: 100vh;

}

.header {

background: linear-gradient(135deg, #5eb045 0%, #4a9c3a 100%);

border-radius: 20rpx;

padding: 30rpx;

margin-bottom: 20rpx;

color: white;

}

.school-info {

display: flex;

flex-direction: column;

align-items: center;

margin-bottom: 30rpx;

}

.school-logo {

width: 200rpx;

height: 100rpx;

margin-bottom: 10rpx;

}

.school-name {

font-size: 28rpx;

font-weight: 600;

}

.student-info {

display: flex;

align-items: center;

}

.student-avatar {

width: 120rpx;

height: 120rpx;

border-radius: 60rpx;

background-color: rgba(255, 255, 255, 0.2);

display: flex;

align-items: center;

justify-content: center;

margin-right: 20rpx;

overflow: hidden;

}

.student-avatar image {

width: 100%;

height: 100%;

}

.avatar-placeholder {

color: white;

font-size: 24rpx;

}

.student-details {

flex: 1;

}

.student-name {

font-size: 32rpx;

font-weight: 600;

margin-bottom: 10rpx;

}

.student-id {

font-size: 24rpx;

opacity: 0.8;

margin-bottom: 5rpx;

}

.parent-info {

font-size: 22rpx;

opacity: 0.7;

}

.today-info {

background: white;

border-radius: 15rpx;

padding: 30rpx;

margin-bottom: 20rpx;

box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);

}

.date {

font-size: 28rpx;

font-weight: 600;

color: var(--primary-color);

margin-bottom: 20rpx;

}

.attendance-status {

display: flex;

gap: 30rpx;

}

.status-item {

display: flex;

align-items: center;

}

.status-label {

font-size: 24rpx;

color: #666;

margin-right: 10rpx;

}

.status-time {

font-size: 24rpx;

font-weight: 600;

color: var(--primary-color);

}

.student-list {

background: white;

border-radius: 15rpx;

padding: 30rpx;

margin-bottom: 20rpx;

box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);

}

.list-title {

font-size: 28rpx;

font-weight: 600;

color: var(--primary-color);

margin-bottom: 20rpx;

}

.student-items {

display: flex;

flex-wrap: wrap;

gap: 15rpx;

}

.student-item {

padding: 15rpx 25rpx;

border-radius: 25rpx;

background-color: #f5f5f5;

color: #666;

font-size: 24rpx;

transition: all 0.3s;

}

.student-item.active {

background-color: var(--primary-color);

color: white;

}

.actions {

display: flex;

gap: 20rpx;

margin-bottom: 30rpx;

}

.action-btn {

flex: 1;

height: 80rpx;

line-height: 80rpx;

border-radius: 40rpx;

font-size: 28rpx;

font-weight: 600;

border: none;

}

.add-student {

background: var(--gradient-green);

color: white;

}

.logout {

background: #ff6b6b;

color: white;

}

.calendar-section {

background: white;

border-radius: 15rpx;

padding: 30rpx;

box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);

}

.calendar-header {

display: flex;

align-items: center;

justify-content: space-between;

margin-bottom: 30rpx;

}

.month-btn {

width: 60rpx;

height: 60rpx;

border-radius: 30rpx;

background-color: #f5f5f5;

display: flex;

align-items: center;

justify-content: center;

font-size: 32rpx;

color: var(--primary-color);

border: none;

}

.month-year {

font-size: 32rpx;

font-weight: 600;

color: var(--primary-color);

}

.calendar-grid {

width: 100%;

}

.weekdays {

display: flex;

margin-bottom: 20rpx;

}

.weekday {

flex: 1;

text-align: center;

font-size: 24rpx;

color: #666;

font-weight: 600;

padding: 10rpx 0;

}

.days {

display: flex;

flex-wrap: wrap;

}

.day {

width: calc(100% / 7);

height: 80rpx;

display: flex;

align-items: center;

justify-content: center;

font-size: 24rpx;

color: #666;

position: relative;

border-radius: 8rpx;

margin: 2rpx;

}

.day.current-month {

color: #333;

}

.day.today {

background-color: var(--primary-color);

color: white;

font-weight: 600;

}

.day.has-checkin {

background-color: rgba(94, 176, 69, 0.1);

}

.checkin-indicator {

position: absolute;

bottom: 5rpx;

right: 5rpx;

}

.indicator-dot {

width: 12rpx;

height: 12rpx;

border-radius: 6rpx;

background-color: var(--primary-color);

}

.indicator-dot.entry {

background-color: #4CAF50;

}

.indicator-dot.exit {

background-color: #FF9800;

}

.indicator-dot.both {

background-color: #2196F3;

}

checkin.js

const api = require('../../utils/api');

Page({

data: {

studentInfo: null,

studentList: [],

currentStudentId: '',

currentStudent: {},

checkinHistory: [],

currentYear: new Date().getFullYear(),

currentMonth: new Date().getMonth() + 1,

calendarDays: [],

studentAttendance: {},

todayDate: '',

todayAttendance: {},

loading: false,

error: null

},

onLoad: function() {

console.log('签到页面加载完成')

this.loadStudentList()

this.loadStudentAttendance()

this.generateCalendar()

this.initTodayInfo()

},

onShow: function() {

console.log('签到页面显示')

// 重新加载学生列表,以便显示新添加的学生

this.loadStudentList()

this.loadStudentAttendance()

this.generateCalendar()

this.initTodayInfo()

},

// 加载学生列表

loadStudentList: function() {

var that = this

var app = getApp()

this.setData({ loading: true, error: null })

// 先尝试从全局数据获取

if (app.globalData.studentList && app.globalData.studentList.length > 0) {

this.setData({

studentList: app.globalData.studentList,

currentStudentId: app.globalData.currentStudentId || app.globalData.studentList[0].studentId,

currentStudent: this.getStudentById(app.globalData.studentList, app.globalData.currentStudentId || app.globalData.studentList[0].studentId),

loading: false

})

return

}

// 从API获取学生数据

api.getAllStudents().then(function(response) {

if (response.success) {

var studentList = response.data || []

var currentStudentId = studentList.length > 0 ? studentList[0].studentId : ''

that.setData({

studentList: studentList,

currentStudentId: currentStudentId,

currentStudent: that.getStudentById(studentList, currentStudentId),

loading: false

})

// 保存到全局数据和本地存储

app.globalData.studentList = studentList

app.globalData.currentStudentId = currentStudentId

wx.setStorage({

key: 'studentList',

data: studentList

})

} else {

throw new Error(response.message || '获取学生数据失败')

}

}).catch(function(error) {

console.error('获取学生数据失败:', error)

// 如果API失败,尝试从本地存储获取

wx.getStorage({

key: 'studentList',

success: function(res) {

var studentList = res.data || []

if (studentList.length > 0) {

var currentStudentId = studentList[0].studentId

that.setData({

studentList: studentList,

currentStudentId: currentStudentId,

currentStudent: that.getStudentById(studentList, currentStudentId),

loading: false,

error: '网络连接失败,使用本地数据'

})

app.globalData.studentList = studentList

app.globalData.currentStudentId = currentStudentId

} else {

// 如果本地也没有数据,生成模拟数据

that.generateMockStudentList()

}

},

fail: function() {

// 如果本地也没有数据,生成模拟数据

that.generateMockStudentList()

}

})

})

},

// 生成模拟学生列表

generateMockStudentList: function() {

var mockStudents = [

{

studentId: '10001',

studentName: '陈奕宏',

parentName: '陈柯辰',

relation: '父亲'

},

{

studentId: '10002',

studentName: '陈奕欣',

parentName: '陈柯辰',

relation: '父亲'

}

]

this.setData({

studentList: mockStudents,

currentStudentId: mockStudents[0].studentId,

currentStudent: mockStudents[0]

})

// 保存到本地存储和全局数据

var app = getApp()

wx.setStorage({

key: 'studentList',

data: mockStudents

})

app.globalData.studentList = mockStudents

app.globalData.currentStudentId = mockStudents[0].studentId

},

// 加载学生考勤数据

loadStudentAttendance: function() {

// 模拟数据

var attendance = {}

var today = new Date()

for (var i = 0; i < 30; i++) {

var date = new Date(today)

date.setDate(date.getDate() - i)

var dateStr = this.formatDate(date)

var hasEntry = Math.random() > 0.1

var hasExit = Math.random() > 0.2

if (hasEntry || hasExit) {

attendance[dateStr] = {

entry: hasEntry ? this.generateTime(7, 8) : null,

exit: hasExit ? this.generateTime(15, 17) : null

}

}

}

this.setData({

studentAttendance: attendance

})

},

// 生成随机时间

generateTime: function(startHour, endHour) {

var hour = startHour + Math.floor(Math.random() * (endHour - startHour + 1))

var minute = Math.floor(Math.random() * 60)

return hour.toString().padStart(2, '0') + ':' + minute.toString().padStart(2, '0')

},

// 格式化日期

formatDate: function(date) {

var year = date.getFullYear()

var month = (date.getMonth() + 1).toString().padStart(2, '0')

var day = date.getDate().toString().padStart(2, '0')

return year + '-' + month + '-' + day

},

// 生成日历数据

generateCalendar: function() {

var currentYear = this.data.currentYear

var currentMonth = this.data.currentMonth

var studentAttendance = this.data.studentAttendance

var calendarDays = []

var firstDay = new Date(currentYear, currentMonth - 1, 1)

var lastDay = new Date(currentYear, currentMonth, 0)

var firstDayOfWeek = firstDay.getDay()

// 添加上个月的日期

var prevMonth = new Date(currentYear, currentMonth - 2, 0)

for (var i = firstDayOfWeek - 1; i >= 0; i--) {

var day = prevMonth.getDate() - i

calendarDays.push({

day: day,

isCurrentMonth: false,

hasCheckin: false,

isToday: false

})

}

// 添加当月的日期

var today = new Date()

for (var day = 1; day <= lastDay.getDate(); day++) {

var dateStr = this.formatDate(new Date(currentYear, currentMonth - 1, day))

var attendance = studentAttendance[dateStr] || {}

var hasEntry = !!attendance.entry

var hasExit = !!attendance.exit

var hasCheckin = hasEntry || hasExit

var checkinType = ''

if (hasEntry && hasExit) {

checkinType = 'both'

} else if (hasEntry) {

checkinType = 'entry'

} else if (hasExit) {

checkinType = 'exit'

}

calendarDays.push({

day: day,

isCurrentMonth: true,

hasCheckin: hasCheckin,

checkinType: checkinType,

isToday: today.getFullYear() === currentYear &&

today.getMonth() + 1 === currentMonth &&

today.getDate() === day

})

}

// 添加下个月的日期

var remainingDays = 42 - calendarDays.length

for (var day = 1; day <= remainingDays; day++) {

calendarDays.push({

day: day,

isCurrentMonth: false,

hasCheckin: false,

isToday: false

})

}

this.setData({

calendarDays: calendarDays

})

},

// 初始化今天信息

initTodayInfo: function() {

var today = new Date()

var todayDateStr = this.formatDate(today)

var todayAttendance = this.data.studentAttendance[todayDateStr] || {}

var year = today.getFullYear()

var month = today.getMonth() + 1

var day = today.getDate()

var weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']

var weekday = weekdays[today.getDay()]

this.setData({

todayDate: year + '年' + month + '月' + day + '日 ' + weekday,

todayAttendance: todayAttendance

})

},

// 修改头像

changeAvatar: function(e) {

console.log('头像被点击', e)

wx.showActionSheet({

itemList: ['拍照', '从相册选择'],

success: function(res) {

if (res.tapIndex === 0) {

// 拍照

this.chooseImage('camera')

} else if (res.tapIndex === 1) {

// 从相册选择

this.chooseImage('album')

}

}.bind(this)

})

},

// 选择图片

chooseImage: function(sourceType) {

wx.chooseImage({

count: 1,

sizeType: ['compressed'],

sourceType: [sourceType],

success: function(res) {

var tempFilePath = res.tempFilePaths[0]

this.uploadAvatar(tempFilePath)

}.bind(this),

fail: function(err) {

console.error('选择图片失败:', err)

wx.showToast({

title: '选择图片失败',

icon: 'none'

})

}

})

},

// 上传头像

uploadAvatar: function(filePath) {

wx.showLoading({

title: '上传中...'

})

// 模拟上传过程

setTimeout(function() {

wx.hideLoading()

// 更新当前学生头像

var studentList = this.data.studentList.map(function(student) {

if (student.studentId === this.data.currentStudentId) {

var newStudent = {}

for (var key in student) {

newStudent[key] = student[key]

}

newStudent.avatar = filePath

return newStudent

}

return student

}.bind(this))

var newCurrentStudent = {}

for (var key in this.data.currentStudent) {

newCurrentStudent[key] = this.data.currentStudent[key]

}

newCurrentStudent.avatar = filePath

this.setData({

studentList: studentList,

currentStudent: newCurrentStudent

})

// 保存到本地存储

wx.setStorage({

key: 'studentList',

data: studentList

})

wx.showToast({

title: '头像更新成功',

icon: 'success'

})

}.bind(this), 1500)

},

// 添加学生

addStudent: function(e) {

console.log('添加学生按钮被点击', e)

wx.showModal({

title: '添加学生',

content: '是否要添加新学生?添加后需要绑定学生信息。',

success: function(res) {

if (res.confirm) {

// 跳转到注册页面

wx.navigateTo({

url: '/pages/register/register'

})

}

}

})

},

// 切换学生

switchStudent: function(e) {

var studentId = e.currentTarget.dataset.studentId

var currentStudent = this.getStudentById(this.data.studentList, studentId)

this.setData({

currentStudentId: studentId,

currentStudent: currentStudent

})

// 重新加载该学生的考勤数据

this.loadStudentAttendance()

// 显示切换成功提示

wx.showToast({

title: '已切换到' + currentStudent.studentName,

icon: 'success',

duration: 1500

})

},

// 根据ID获取学生信息

getStudentById: function(studentList, studentId) {

for (var i = 0; i < studentList.length; i++) {

if (studentList[i].studentId === studentId) {

return studentList[i]

}

}

return studentList[0]

},

// 上一个月

prevMonth: function() {

var currentYear = this.data.currentYear

var currentMonth = this.data.currentMonth

currentMonth--

if (currentMonth < 1) {

currentMonth = 12

currentYear--

}

this.setData({

currentYear: currentYear,

currentMonth: currentMonth

})

this.generateCalendar()

},

// 下一个月

nextMonth: function() {

var currentYear = this.data.currentYear

var currentMonth = this.data.currentMonth

currentMonth++

if (currentMonth > 12) {

currentMonth = 1

currentYear++

}

this.setData({

currentYear: currentYear,

currentMonth: currentMonth

})

this.generateCalendar()

},

// 点击日期

onDayTap: function(e) {

var day = e.currentTarget.dataset.day

var currentYear = this.data.currentYear

var currentMonth = this.data.currentMonth

var dateStr = this.formatDate(new Date(currentYear, currentMonth - 1, day))

var attendance = this.data.studentAttendance[dateStr]

if (attendance) {

var message = currentYear + '年' + currentMonth + '月' + day + '日\n'

if (attendance.entry) {

message += '入校时间: ' + attendance.entry + '\n'

}

if (attendance.exit) {

message += '出校时间: ' + attendance.exit

}

wx.showModal({

title: '考勤详情',

content: message,

showCancel: false

})

} else {

wx.showToast({

title: '该日无考勤记录',

icon: 'none'

})

}

},

// 退出登录

testLogout: function(e) {

console.log('退出登录按钮被点击', e)

wx.showModal({

title: '确认退出',

content: '确定要退出登录吗?',

success: function(res) {

if (res.confirm) {

wx.showToast({

title: '正在退出...',

icon: 'loading',

duration: 1500

})

setTimeout(function() {

// 清除本地数据

wx.clearStorageSync()

// 跳转到登录页面

wx.reLaunch({

url: '/pages/login/login',

success: function() {

console.log('成功跳转到登录页面')

},

fail: function(err) {

console.error('跳转失败:', err)

wx.showToast({

title: '跳转失败',

icon: 'none'

})

}

})

}, 1500)

}

}

})

}

})

6-1-4 工具文件 (utils/)

api.js

// API配置

const API_BASE_URL = 'https://zrjm.cc/api';

// 请求封装

function request(options) {

return new Promise((resolve, reject) => {

wx.request({

url: API_BASE_URL + options.url,

method: options.method || 'GET',

data: options.data || {},

header: {

'Content-Type': 'application/json',

...options.header

},

success: function(res) {

if (res.statusCode === 200) {

resolve(res.data);

} else {

reject(new Error(`请求失败: ${res.statusCode}`));

}

},

fail: function(err) {

reject(err);

}

});

});

}

// API接口

const api = {

// 获取所有学生数据

getAllStudents() {

return request({

url: '/students',

method: 'GET'

});

},

// 根据学生ID获取学生信息

getStudentById(studentId) {

return request({

url: /students/${studentId},

method: 'GET'

});

},

// 验证学生ID是否存在

verifyStudentId(studentId) {

return request({

url: '/students/verify',

method: 'POST',

data: { studentId }

});

},

// 根据姓名搜索学生

searchStudentsByName(name) {

return request({

url: /students/search/${encodeURIComponent(name)},

method: 'GET'

});

},

// 获取指定年级的学生

getStudentsByGrade(grade) {

return request({

url: /students/grade/${grade},

method: 'GET'

});

},

// 获取指定班级的学生

getStudentsByClass(className) {

return request({

url: /students/class/${encodeURIComponent(className)},

method: 'GET'

});

},

// 签到记录

checkin(studentId, checkinType) {

return request({

url: '/checkin',

method: 'POST',

data: {

studentId,

checkinType,

timestamp: new Date().toISOString()

}

});

}

};

module.exports = api;

6-1-5 全局配置文件

app.js

App({

onLaunch: function() {

// 展示本地存储能力

const logs = wx.getStorageSync('logs') || []

logs.unshift(Date.now())

wx.setStorageSync('logs', logs)

// 登录

wx.login({

success: res => {

// 发送 res.code 到后台换取 openId, sessionKey, unionId

}

})

},

globalData: {

userInfo: null,

studentInfo: null,

studentList: [],

currentStudentId: null

}

})

app.json

{

"pages": [

"pages/login/login",

"pages/register/register",

"pages/checkin/checkin"

],

"window": {

"backgroundTextStyle": "light",

"navigationBarBackgroundColor": "#5eb045",

"navigationBarTitleText": "家长接送签到系统",

"navigationBarTextStyle": "white"

},

"style": "v2",

"sitemapLocation": "sitemap.json"

}

app.wxss

/**app.wxss**/

.container {

height: 100%;

display: flex;

flex-direction: column;

align-items: center;

justify-content: space-between;

padding: 200rpx 0;

box-sizing: border-box;

}

/* 全局变量 */

page {

--primary-color: #5eb045;

--light-green: #f0f9e8;

--text-color: #333;

--gradient-green: linear-gradient(135deg, #5eb045 0%, #4a9c3a 100%);

}

/* 全局样式重置 */

* {

box-sizing: border-box;

}

button {

border: none;

outline: none;

}

button::after {

border: none;

}

input {

outline: none;

}

/* 通用样式 */

.text-center {

text-align: center;

}

.mb-20 {

margin-bottom: 20rpx;

}

.mt-20 {

margin-top: 20rpx;

}

.p-20 {

padding: 20rpx;

}

.flex {

display: flex;

}

.flex-center {

display: flex;

align-items: center;

justify-content: center;

}

.flex-between {

display: flex;

align-items: center;

justify-content: space-between;

}

.flex-column {

display: flex;

flex-direction: column;

}

.flex-1 {

flex: 1;

}

/* 文字样式 */

.text-primary {

color: var(--primary-color);

}

.text-white {

color: white;

}

.text-gray {

color: #666;

}

.text-small {

font-size: 24rpx;

}

.text-large {

font-size: 32rpx;

}

.text-bold {

font-weight: 600;

}

/* 背景样式 */

.bg-primary {

background-color: var(--primary-color);

}

.bg-light {

background-color: var(--light-green);

}

.bg-white {

background-color: white;

}

/* 圆角样式 */

.rounded {

border-radius: 8rpx;

}

.rounded-lg {

border-radius: 15rpx;

}

.rounded-full {

border-radius: 50%;

}

/* 阴影样式 */

.shadow {

box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);

}

.shadow-lg {

box-shadow: 0 8rpx 16rpx rgba(0, 0, 0, 0.15);

}

---

7. 域名配置

7-1. 微信公众平台配置

7-1-1 登录微信公众平台

1. 访问:https://mp.weixin.qq.com/

2. 使用管理员微信扫码登录

7-1-2 配置服务器域名

1. 进入"开发" → "开发管理" → "开发设置"

2. 找到"服务器域名"部分

3. 填写以下域名:

```

request合法域名: https://zrjm.cc

socket合法域名: (留空)

uploadFile合法域名: https://zrjm.cc

downloadFile合法域名: https://zrjm.cc

udp合法域名: (留空)

tcp合法域名: (留空)

DNS预解析域名: zrjm.cc

预连接域名: https://zrjm.cc

```

7-1-3 保存配置

1. 点击"保存并提交"

2. 等待配置生效(通常几分钟内)

---

8. 测试与调试

8-1. 本地测试

8-1-1 在微信开发者工具中测试

1. 打开微信开发者工具

2. 选择"小程序"

3. 导入项目:选择F:\Etu目录

4. 点击"编译"

8-1-2 功能测试清单

注册功能测试:

  • - 页面正常显示

  • - 表单验证正常

  • - 学生ID验证功能

  • - 成功案例:学生ID 10001,姓名 陈奕宏

  • - 成功案例:学生ID 10002,姓名 陈奕欣

  • - 失败案例:错误的学生ID或姓名

  • - 跳转功能正常

签到功能测试:

  • - 页面正常显示

  • - 学生信息显示正确

  • - 日历功能正常

  • - 切换学生功能

  • - 添加学生功能

  • - 退出登录功能

8-1-3 网络请求测试

1. 打开"调试器" → "Network"面板

2. 测试注册功能

3. 检查请求是否发送到 https://zrjm.cc/api/students/verify

4. 验证响应数据格式

8-2. 真机测试

8-2-1 预览功能

1. 在微信开发者工具中点击"预览"

2. 用微信扫描二维码

3. 在真机上测试功能

8-2-2 真机调试

1. 在微信开发者工具中点击"真机调试"

2. 用微信扫描二维码

3. 在真机上测试并查看调试信息

---

9. 发布上线

9-1. 代码上传

9-1-1 在微信开发者工具中上传

1. 点击"上传"按钮

2. 填写版本信息:

- 版本号:1.0.0

- 项目备注:初始版本发布

3. 点击"上传"

9-1-2 确认上传成功

1. 登录微信公众平台

2. 进入"开发" → "开发管理"

3. 查看"开发版本"列表

4. 确认代码已上传

9-2. 提交审核

9-2-1 填写审核信息

1. 在"开发版本"中找到上传的版本

2. 点击"提交审核"

3. 填写审核信息:

- 功能页面:选择主要功能页面

- 测试账号:提供测试账号(如需要)

- 补充材料:上传相关说明文档

9-2-2 等待审核

1. 提交后等待微信审核

2. 审核时间:通常1-7个工作日

3. 审核结果会通过微信通知

9-3. 发布上线

9-3-1 审核通过后发布

1. 收到审核通过通知

2. 在微信公众平台点击"发布"

3. 确认发布信息

4. 点击"发布"

9-3-2 验证发布结果

1. 在微信中搜索小程序名称

2. 测试所有功能

3. 确认用户体验正常

---

10. 后续优化

10-1. 飞书接口集成

10-1-1 申请飞书应用

1. 访问飞书开放平台:https://open.feishu.cn/

2. 创建企业自建应用

3. 获取应用凭证:

- App ID

- App Secret

- 表格Token

- 表格ID

10-1-2 配置后端服务

1. 更新后端代码集成飞书API

2. 配置字段映射

3. 测试数据同步

10-1-3 更新小程序

1. 移除模拟验证逻辑

2. 使用真实API接口

3. 测试完整功能

10-2. 功能增强

10-2-1 数据统计

- 添加考勤统计功能

- 生成月度报表

- 数据可视化展示

10-2-2 消息通知

- 签到成功通知

- 异常情况提醒

- 定期数据推送

10-2-3 权限管理

- 管理员功能

- 教师查看权限

- 家长权限控制

10-3. 性能优化

10-3-1 代码优化

- 图片压缩优化

- 代码分包加载

- 缓存策略优化

10-3-2 服务器优化

- 数据库连接池

- Redis缓存

- CDN加速

10-4. 安全加固

10-4-1 API安全

- 接口签名验证

- 频率限制

- 数据加密

10-4-2 数据安全

- 敏感信息脱敏

- 数据备份策略

- 访问日志记录

---

总结

本教程详细介绍了从零开始开发微信小程序到成功发布的完整流程,包括:

1. 环境准备:开发工具、服务器、域名等

2. 项目开发:前端小程序、后端API、数据库设计

3. 服务器部署:环境配置、SSL证书、服务管理

4. 功能测试:本地测试、真机测试、网络调试

5. 发布上线:代码上传、审核提交、正式发布

6. 后续优化:功能增强、性能优化、安全加固