自助发号

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位)
    微信获取 需要用户授权timecard_id
  • 管理员 admin

    账号(string)密码(sha1加密)状态 (bool)
    用户账号加密类型 sha11 登陆 0 禁止登陆
  • 地区表

    地区id
    地区
2-1-4 业务流程分析
  • 点击购买对应的卡密 - 数据库里面筛选 相应价格区间 及未激活的卡密组
  • 完成付款则提交订单 - 此时购买详情表新增购买数据 - 此时卡密表对应的卡密为激活状态
  • 返回订单页面 - 查看购买详情 - 查看购买对应的卡密 - 返回卡密表中card_id并且为激活状态 的卡密
  • 个人可以查看购买历史

5 接口设计

  • 管理员 admin

    业务路由提交方式参数
    登陆/loginpost用户名(username),密码(password)
  • 卡密表 commodity

    业务路由提交方式参数
    查询/commodityget价格(price),状态(state)
    添加卡密/commodity/insertpost卡密(),区域(),价格(price),默认未激活,card_id
    更改数据/commodity/:id(id 主键自增)patch卡密(),类型(),价格(price)激活
    删除卡密/commodity/:id(id 主键自增)deleteid(主键自增)
    查询地区价格/commodity/areaget选择地区后 获取该地区的价格区间
  • 具体购买信息 info

    业务路由提交方式参数
    查询/infoget购买时间(time),card_id
    增加/info/insertpost用户名,购买时间(自动获得),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.项目结束

最后压缩,打包备份仓库吃灰

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值