node+mysql+vue+element+wx小程序
1.前言
疫情在家,有个朋友跑去非洲,那边充值需要购买卡密不是很方便,本人就产生制作这么一个项目的想法。
2.详情
2-1.整个项目的业务逻辑
2-1-1设计思想
- 充值需求 : 充值业务
- 进销库存的设计思维:
- 进: 手动录入卡密
- 销: 购买自动发送卡密
2-1-2 业务分析
- 服务端 后台数据录入的渠道
- 登陆 : 指定用户
- 注册 : 非开放状态,不提供注册
- 状态 :正常登陆或者禁止登陆
- 用户端
- 自动获取登陆用户名信息(需要获取用户授权)
- 购买卡密(自动发货)
2-1-3 数据库 结构设计
-
卡密表 commodity
卡密 (varchar) 地区(varchar) 价格 ( double) 状态 (bool) card _id (int) 凭证 充值区域 价格 1:激活(0:未激活) 用于用户关联 (自增) -
具体购买信息表 info
用户名(varchar) 购买时间(data) 购买卡密的id(卡密前后4位) 微信获取 需要用户授权 time card_id -
管理员 admin
账号(string) 密码(sha1加密) 状态 (bool) 用户账号 加密类型 sha1 1 登陆 0 禁止登陆 -
地区表
地区 id 地区
2-1-4 业务流程分析
- 点击购买对应的卡密 - 数据库里面筛选 相应价格区间 及未激活的卡密组
- 完成付款则提交订单 - 此时购买详情表新增购买数据 - 此时卡密表对应的卡密为激活状态
- 返回订单页面 - 查看购买详情 - 查看购买对应的卡密 - 返回卡密表中card_id并且为激活状态 的卡密
- 个人可以查看购买历史
5 接口设计
-
管理员 admin
业务 路由 提交方式 参数 登陆 /login post 用户名(username),密码(password) -
卡密表 commodity
业务 路由 提交方式 参数 查询 /commodity get 价格(price),状态(state) 添加卡密 /commodity/insert post 卡密(),区域(),价格(price),默认未激活,card_id 更改数据 /commodity/:id(id 主键自增) patch 卡密(),类型(),价格(price)激活 删除卡密 /commodity/:id(id 主键自增) delete id(主键自增) 查询地区价格 /commodity/area get 选择地区后 获取该地区的价格区间 -
具体购买信息 info
业务 路由 提交方式 参数 查询 /info get 购买时间(time),card_id 增加 /info/insert post 用户名,购买时间(自动获得),card_id(点击购买时获得这个参数) -
地区 area
业务 路由 参数 查询地区 /area 无 新增地区 /area 地区 area
2-2.node+mysql 数据处理
- 进来就上路由页面
router
.post('/session',sessionController.create)
// 地区管理
router
.get('/area',areaController.list)
.post('/area',CheckLogin,areaController.create)
// 卡密商品管理
router
.get('/commodity',commodityController.list)
.get('/commoditys',CheckLogin,commodityController.alllist)
.get('/commodity/price',commodityController.getprice)
.post('/commodity',CheckLogin,commodityController.create)
.patch('/commodity',CheckLogin,commodityController.update)
.delete('/commodity',CheckLogin,commodityController.delete)
// 购买详情页面
router
.get('/info',infoController.list)
.post('/info',infoController.create)
.get('/info/cdkey',infoController.findcdkey)
中间加了个验证登陆的中间件
const CheckLogin = (req,res,next) => {
const { user } = req.session
if (!user) {
return res.status(401).json({
error:'Unauthorized'
})
}
next()
}
忽略我随意起名和命名的不规范
这个是目录结构
- model : db用于连接数据库的 hmac :加密密码的 基本是sha256
//创建连接池
const pool = mysql.createPool({
host:'localhost',
user:'root',
password:'123456',
database:'auto_cdkey'
})
连接就很简单了
- controllers 处理用户发过来的请求
例如登陆
// 用户创建会话
// 登陆请求
exports.create = async (req,res,next) => {
try {
const body = req.body
body.password = hmac.result(body.password)
const sqlStr = `SELECT * FROM admin WHERE username = '${body.username}' AND password = '${body.password}' AND state = 1`
const [user] = await db.query(sqlStr)
if(!user){
return res.status(404).json({
error:'账户或者密码错误或者用户被封禁'
})
}
req.session.user = user
res.status(200).json({})
} catch(e) {
next(e)
}
}
地区管理
const db = require('../models/db')
const hmac = require('../models/hmac')
// 获取地区
exports.list = async (req,res,next) => {
try {
sqlStr = `
SELECT area FROM area
`
const area = await db.query(sqlStr)
res.status(200).json(area)
} catch(e) {
next(e)
}
}
// 新增地区
exports.create = async (req,res,next) => {
try {
const {area} = req.body
const [ret] = await db.query(`SELECT * FROM area WHERE area = '${area}'`)
sqlStr = `
INSERT INTO area (area) VALUES ('${area}')
`
if(ret){
return res.status(200).json({
error:"area exist"
})
}
// 验证是否新增成功
const {insertId} = await db.query(sqlStr)
const [insert] = await db.query(`SELECT * FROM area WHERE id = ${insertId}`)
if (!insert) {
return res.status(500).json({
error: "INTERNAL SERVER ERROR"
})
}
res.status(201).json({})
} catch(e) {
next(e)
}
}
写的比较简单 还是用的拼接字符串 [狗头保命] 感觉被注入的风险高
2-2.vue-element 管理界面
-
老登陆界面了 还是bootstrap社区文档的案例
-
主页面
饿了么的框架挺舒服 这个简单 ctrl + C ctrl + V -
目录结构
-
没错 不使用我之前写的webpack打包的方案 直接用cli 偷懒
-
路由 没错就两个页面
import Vue from 'vue' import VueRouter from 'vue-router' import Login from '../views/Login.vue' import Home from '../views/Home.vue' Vue.use(VueRouter) const routes = [ { path: '/', name: 'login', component: Login }, { path: '/Home', name: 'home', component: Home } ] const router = new VueRouter({ routes }) export default router
-
为啥就两个页面呢 添加 编辑都是使用el-drawer
基本就是这个样子
<template>
<el-table
:data="tableData"
style="width: 100%"
:default-sort = "{prop: 'date', order: 'descending'}"
:row-class-name="tableRowClassName">
<el-table-column type="index" label="#" width="80">
</el-table-column>
<el-table-column prop="cdkey" label="卡密" width="360">
</el-table-column>
<el-table-column
prop="area"
label="地区"
width="220"
:filters="area"
:filter-method="filterHandler">
</el-table-column>
<el-table-column prop="price" label="价格" sortable width="200">
</el-table-column>
<el-table-column
label="状态"
width="200"
prop="state"
:filters="[{text: '激活', value: 1}, {text: '未激活', value: 0}]"
:filter-method="filterHandler">
<template slot-scope="scope" >
<span v-show="scope.row.state == 1">激活</span>
<span v-show="scope.row.state == 0">未激活</span>
</template>
</el-table-column>
<el-table-column
fixed="right"
label="操作"
width="200"
>
<template slot-scope="scope">
<!-- 编辑 -->
<el-button
size="mini"
type="primary"
icon="el-icon-edit"
circle
@click="handleEdit(scope.$index, scope.row)">
</el-button>
<!-- 删除 -->
<el-button
size="mini"
type="danger"
icon="el-icon-delete"
circle
@click="handleDelete(scope.$index, scope.row, tableData)">
</el-button>
</template>
<template>
<!-- 编辑页面 -->
<el-drawer title="编辑条目"
:before-close="handleClose"
:visible.sync="dialog"
direction="ltr"
custom-class="demo-drawer"
append-to-body
close-on-press-escape>
<div class="demo-drawer__content">
<el-form>
<el-form-item label="卡密" :label-width="formLabelWidth">
<el-input v-model="editdata.cdkey" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="地区" :label-width="formLabelWidth">
<el-select v-model="editdata.area" placeholder="请选择地区">
<el-option v-for="item in area" :key="item.value" :label="item.text" :value="item.value">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="价格" :label-width="formLabelWidth">
<el-input v-model="editdata.price" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="状态" :label-width="formLabelWidth">
<el-switch v-model="editdata.state" :active-value="1" :inactive-value="0" active-text="激活" inactive-text="未激活"></el-switch>
</el-form-item>
<div class="demo-drawer__footer">
<el-button @click="cancelForm" size="medium">取 消</el-button>
<el-button type="primary" size="medium" @click="updateedit()" :loading="loading">{{ loading ? '提交中 ...' : '确 定' }}
</el-button>
</div>
</el-form>
</div>
</el-drawer>
</template>
</el-table-column>
</el-table>
</template>
<script>
export default {
props: ['tableData'],
data () {
return {
// 地区数据
area: [],
// 编辑条目
editdata: {},
// drawer 状态
dialog: false,
loading: false,
formLabelWidth: '80px',
timer: null
}
},
async created () {
// 获取地区数据
const { data } = await this.$axios.get('/area')
// 遍历数据
const that = this
data.forEach(function (element, index) {
that._data.area.push({ 'text': element.area, 'value': element.area })
})
},
methods: {
// 编辑
handleEdit (index, row) {
// 深拷贝 防止污染父组件值
row.id = index
this.editdata = JSON.parse(JSON.stringify(row))
this.dialog = true
},
// 删除
handleDelete (index, row, rows) {
try {
this.$confirm(`是否永久删除'${row.cdkey}-${row.area}'`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
await this.$axios.delete(`/commodity?card_id=${row.card_id}`)
this.$message.success('成功删除')
// 移除当前行
rows.splice(index, 1)
} catch (e) {
console.log(e)
}
}).catch(async () => {
console.log('3')
this.$message({
type: 'info',
message: '已取消删除'
})
})
} catch (e) {
console.log(e)
}
},
// 筛选条件(过滤选项)
filterHandler (value, row, column) {
const property = column['property']
return row[property] === value
},
tableRowClassName ({ row, rowIndex }) {
// 激活状态更改颜色状态
if (row.state === 1) {
return 'warning-row'
}
return ''
},
handleClose (done) {
if (this.loading) {
return
}
this.$confirm('确定要提交表单吗?')
.then(_ => {
this.loading = true
this.timer = setTimeout(() => {
done()
// 动画关闭需要一定的时间
setTimeout(() => {
this.loading = false
}, 400)
this.updateedit()
}, 2000)
})
.catch(_ => {})
},
cancelForm () {
this.loading = false
this.dialog = false
clearTimeout(this.timer)
},
// 编辑后更新数据
async updateedit () {
try {
const { data } = await this.$axios.patch('/commodity', this.editdata)
if (!data) {
return this.$message.error('服务异常,请稍后重试!')
}
this.tableData[this.editdata.id] = data
this.$message.success('更新完成')
setTimeout(() => {
this.dialog = false
}, 400)
} catch (e) {
throw e
}
}
}
}
</script>
<style>
.el-table .warning-row {
background: #ABABAB;
}
.demo-drawer__footer {
position: absolute;
bottom: 20px;
width: 100%;
}
.demo-drawer__footer button{
position: relative;
width: 45%;
}
</style>
直接放代码
- 跨域问题
module.exports = {
devServer: {
proxy: 'http://127.0.0.1:3000/', //后台数据的端口
port:4000 //页面的端口
}
}
- 最后放一张 main.js element-ui 框架 axios这个不多解释
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import axios from 'axios'
Vue.config.productionTip = false
Vue.use(ElementUI)
Vue.prototype.$axios = axios
new Vue({
router,
render: h => h(App)
}).$mount('#app')
2-3 小程序
- 页面截图
选择购买地区同时获取该地区未被激活的卡号的价格 如果没有也代表没有存货
放代码 - index.wxml
<!--index.wxml-->
<view class="container">
<!-- 头部地区选择 -->
<view class="area" bindtap="changearea">
<text>选择购买地区: </text>
<text>{{areaselect}}</text>
<text class="changearea">[切换地区]</text>
</view>
<!-- 价格区间 -->
<view class="price">
<h2 class="title">请选择需要购买的额度:</h2>
<block wx:if="{{price == ''}}">
<view class="alert-message">抱歉!该地区暂无商品</view>
</block>
<block wx:else>
<block wx:for="{{price}}" wx:key="key" >
<view class="price-menu" bindtap="priceselect" data-id="{{item}}">
<text>{{item}}</text>
</view>
</block>
</block>
</view>
<!-- 提交订单以及订单详情 -->
<block wx:if="{{priceselect}}">
<view class="detail" bindtap="submit">
<text> 订单信息:{{areaselect}}-¥{{priceselect}}</text>
<text class="submit">确认</text>
</view>
</block>
<!-- 弹窗菜单 -->
<mp-actionSheet bindactiontap="btnClick" show="{{showActionsheet}}" actions="{{area}}" title="地区选择" show-cancel cancel-text="取消选择" mask-closable>
</mp-actionSheet>
</view>
- index.js
//index.js
//获取应用实例
const app = getApp()
Page({
data: {
// 地区数据
area:{},
// 当前选择地区
areaselect:null,
// Actionsheet 显示状态
showActionsheet: false,
// 地区价格
price:[],
// 选择购买的价格
priceselect:null
},
onLoad:async function (options) {
const that = this
const area = []
// 缓存获取是否登陆
wx.getStorage({
key: 'userinfo',
success(res) {
//定义了一个全局的 点击购买的时候以此判断 是否登陆 页面初次加载需要从缓存获取信息 然后再赋值给全局的
app.globalData.userinfo = res.data
}
})
// 请求获取地区数据
await wx.request({
url: 'http://127.0.0.1:3000/area',
header: {
'content-type': 'application/json' // 默认值
},
success(res) {
for (let i = 0; i < res.data.length; i++) {
area.push({ text: res.data[i].area, value: res.data[i].area})
}
that.setData({
area: area,
areaselect: area[0].value
})
that.getareaprice(area[0].value)
}
})
},
//地区切换
changearea: function () {
this.setData({
showActionsheet: true
})
},
close: function () {
this.setData({
showActionsheet: false
})
},
btnClick(e) {
this.setData({
areaselect: e.detail.value
})
this.getareaprice(e.detail.value)
this.close()
},
// 获取地区价格
getareaprice:async function (area){
const that = this
const price = []
await wx.request({
url: `http://127.0.0.1:3000/commodity/price?area=${area}`,
header: {
'content-type': 'application/json' // 默认值
},
success(res) {
for (let i = 0; i < res.data.length; i++) {
price.push(res.data[i].price)
}
that.setData({
price: price
})
}
})
},
// 选择价格
priceselect:function (e) {
this.setData({
priceselect: e.currentTarget.dataset.id
})
},
// 订单提交
submit:function (){
let userinfo = app.globalData.userinfo
if(!userinfo){
return wx.switchTab({
url: '/pages/my/my'
})
}else{
this.getcommodity()
}
},
getcommodity:async function(){
// 订单提交 先需要获得支付成功返回的参数 ?area=莫桑比克&price=25&state=0
await wx.request({
url: `http://127.0.0.1:3000/commodity?area=${this.data.areaselect}&price=${this.data.priceselect}&state=0`,
header: {
'content-type': 'application/json' // 默认值
},
success(res) {
// 获取成功前往支付页面 我个人开发无权调用支付界面
app.globalData.card_id = res.data.card_id
// 跳转成功页面
if(res.data){
wx.navigateTo({
url: '/pages/msg/msg'
})
}
}
})
}
})
比如这个 原本有四个 逻辑:这个是获取卡号的id 并非卡号,购买成功后卡号状态为激活,并且新增购买记录 后期通过卡号id获取卡号
登陆的实现
<button open-type="getUserInfo" type="primary" size="mini" bindgetuserinfo="getUserInfo">登陆</button>
<!-- bindgetuserinfo="getUserInfo" 通过e.detail.userInfo 获得用户基本信息 -->
我把用户信息写入缓存
wx.setStorageSync('userinfo', userinfo) //key:'key' ,value: value
如果不需要用户信息 简单的显示基本信息 这样就行 小程序官方文档有介绍
<open-data type="groupName" open-gid="xxxxxx"></open-data>
<open-data type="userAvatarUrl"></open-data>
<open-data type="userGender" lang="zh_CN"></open-data>
- 购买记录
偷懒用的wx-ui 弹出框
<mp-dialog title="卡密详情" show="{{show}}" buttons="{{oneButton}}" bindbuttontap="tapDialogButton">
<view>{{ cdkey }}</view>
</mp-dialog>
show:显示/!显示 bool类型 详情见微信开发文档传送门
3.项目结束
最后压缩,打包备份仓库吃灰