疫情在家,有个朋友跑去非洲,那边充值需要购买卡密不是很方便,本人就产生制作这么一个项目的想法。
卡密表 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 |
---|---|
地区 |
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 |
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()
}
//创建连接池
const pool = mysql.createPool({
host:'localhost',
user:'root',
password:'123456',
database:'auto_cdkey'
})
连接就很简单了
// 用户创建会话
// 登陆请求
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)
}
}
写的比较简单 还是用的拼接字符串 [狗头保命] 感觉被注入的风险高
没错 不使用我之前写的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
<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 //页面的端口
}
}
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')
<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
//获取应用实例
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>
我把用户信息写入缓存
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>
<mp-dialog title="卡密详情" show="{{show}}" buttons="{{oneButton}}" bindbuttontap="tapDialogButton">
<view>{{ cdkey }}</view>
</mp-dialog>
show:显示/!显示 bool类型 详情见微信开发文档传送门
最后压缩,打包备份仓库吃灰