Billing and its related interfaces

preface

Interface is the core module of this practical project. Users can record their daily consumption and income through the billing module. Five interfaces need to be written in this section:

  • The bill list
  • Add bill
  • Modify the bill
  • Delete the billing
  • The billing details

knowledge

  • A set ofCRUD
  • Multilevel complex data structure processing
  • egg-mysqlThe use of

Add billing interface

Need to implement a new bill first, in order to more convenient production of subsequent other interfaces. Let’s review the bill we designed earlier.

You can easily determine which fields are required for the new billing interface based on the properties of the table above, so open /controller, add the bill.js script file to the directory, and add an add method as follows:

'use strict';

const moment = require('moment')

const Controller = require('egg').Controller;

class BillController extends Controller {
  async add() {
    const { ctx, app } = this;
    // Get the parameters carried in the request
    const { amount, type_id, type_name, date, pay_type, remark = ' ' } = ctx.request.body;

    // Void processing, which can also be done on the front end, but the back end also needs to do a layer of judgment.
    if(! amount || ! type_id || ! type_name || ! date || ! pay_type) { ctx.body = {code: 400.msg: 'Parameter error'.data: null}}try {
      let user_id
      const token = ctx.request.header.authorization;
      // Get the token to get the user information
      const decode = app.jwt.verify(token, app.config.jwt.secret);
      if(! decode)return
      user_id = decode.id
      // user_id is added to each bill item by default as an indication of how to obtain the specified user's bill later.
      // If I log in to account A, all the operations I do will be added with account A's ID, and the bill information of account A's ID will be filtered out when I obtain it later.
      const result = await ctx.service.bill.add({
        amount,
        type_id,
        type_name,
        date,
        pay_type,
        remark,
        user_id
      });
      ctx.body = {
        code: 200.msg: 'Request successful'.data: null}}catch (error) {
      ctx.body = {
        code: 500.msg: 'System error'.data: null}}}}module.exports = BillController;
Copy the code

The only thing to note about the new billing interface is that when you write data to the database, you need to bring the user ID, so that it is easy to find, modify and delete the bill information of the corresponding user. Therefore, all interfaces in this section must be filtered by the authentication middleware. You must obtain the token of the current user to obtain the user ID information.

Add bill.js to bill.js/bill.js/bill.js/bill.js/bill.js/bill.js/bill.js/bill.js/bill.js/bill.js

'use strict'; const Service = require('egg').Service; class BillService extends Service { async add(params) { const { app } = this; Return await app.mysql.insert('bill', params); } catch (error) { console.log(error); return null; } } } module.exports = BillService;Copy the code

App.mysql. insert is a database insert operation wrapped in egg mysql. It is an asynchronous method, so there are many places where asynchronous operations are required.

Do not forget to throw the interface, many times after writing logic, forget to throw the interface, a 404 error.

// router.js router.post('/api/bill/add', _jwt, controller.bill.add); // Add the bill
Copy the code

Open debugging interface buddy Postman and verify it:

Note that the following token is required for Headers:

Id is an increment property, so add one piece of data, default is 1, add another piece of data, id is 2.

Where do the type_id and type_name attributes come from?

When adding bills to the list, select the type of bill such as food, shopping, study, bonus, etc. This type of bill is obtained from the type table defined earlier. Here’s how to manually define the initial data for the table, as shown below:

The meaning of each attribute can be returned to Database Table Design for details. Here the user_id attribute is 0, which represents the common billing type available to all users. If custom attributes need to be added later, user_id needs to specify the ID of a user.

Obtain the bill list

To obtain the bill list, you can first check what kind of presentation form the front end needs to make:

Analyzing the figure above, bills are taken as the dimension of time. For example, I recorded two bills on January 1, 2021, and one bill on January 2, 2021. The data returned would look like this:

[{date: '2020-1-1'.bills: [{// bill Each bill in the data table
      },
      {
        // bill Each bill in the data table}]}, {date: '2020-1-2'.bills: [{// bill Each bill in the data table}},]]Copy the code

And the front end needs to do more scrolling loading, so the server needs to be paginated. After retrieving the data in the Bill table, you need to perform a series of operations to consolidate the data into the above format.

Of course, the captured time dimension is in months and can be filtered by billing type. The top left corner of the chart shows total expenses and total income for the month, which is also shown in tabular data because it is strongly correlated with billing data.

So, open /controller/bill.js and add a list method to handle the list of bill data:

// Get the list
async list() {
  const { ctx, app } = this;
  // Get, date, pagination data, type type_id, which are passed from the front end to the back end
  const { date, page = 1, page_size = 5, type_id = 'all' } = ctx.query

  try {
    let user_id
    // Get user_id through token resolution
    const token = ctx.request.header.authorization;
    const decode = app.jwt.verify(token, app.config.jwt.secret);
    if(! decode)return
    user_id = decode.id
    // Get a list of the current user's bills
    const list = await ctx.service.bill.list(user_id)
    console.log('list', list)
    console.log('date', date)
    // Filter out the list of bills for month and type
    const _list = list.filter(item= > {
      if(type_id ! = ='all') {
        return moment(Number(item.date)).format('YYYY-MM') === date && type_id === item.type_id
      }
      return moment(Number(item.date)).format('YYYY-MM') === date
    })
    console.log('_list', _list)
    // Format the data into the previously set object format
    let listMap = _list.reduce((curr, item) = > {
      // curr defaults to an empty array []
      // Format the time of the first bill item as YYYY-MM-DD
      const date = moment(Number(item.date)).format('YYYY-MM-DD')
      // If you can find the current item date in the accumulated array, add the current item to the bills array.
      if (curr && curr.length && curr.findIndex(item= > item.date === date) > -1) {
        const index = curr.findIndex(item= > item.date === date)
        curr[index].bills.push(item)
      }
      // If the current item date is not found in the accumulated array, create a new item.
      if (curr && curr.length && curr.findIndex(item= > item.date === date) === -1) {
        curr.push({
          date,
          bills: [item]
        })
      }
      // If curr is an empty array, the first bill item is added by default, formatted in the following pattern
      if(! curr.length) { curr.push({ date,bills: [item]
        })
      }
      return curr
    }, []).sort((a, b) = > moment(b.date) - moment(a.date)) // The time sequence is in reverse order, the time is about new, above

    // All data formatted by listMap is not paginated yet.
    const filterListMap = listMap.slice((page - 1) * page_size, page * page_size)

    // Calculate total income and expenses for the month
    // First get a list of all bills for that month
    let __list = list.filter(item= > moment(Number(item.date)).format('YYYY-MM') === date)
    // Add up the expenses
    let totalExpense = __list.reduce((curr, item) = > {
      if (item.pay_type === 1) {
        curr += Number(item.amount)
        return curr
      }
      return curr
    }, 0)
    // Add up the income
    let totalIncome = __list.reduce((curr, item) = > {
      if (item.pay_type === 2) {
        curr += Number(item.amount)
        return curr
      }
      return curr
    }, 0)

    // Return data
    ctx.body = {
      code: 200.msg: 'Request successful'.data: {
        totalExpense, // Monthly expenditure
        totalIncome, // Monthly income
        totalPage: Math.ceil(listMap.length / page_size), / / total pages
        list: filterListMap || [] // Formatted and paginated data}}}catch {
    ctx.body = {
      code: 500.msg: 'System error'.data: null}}}Copy the code

The analysis of the code logic, all written in the form of annotations, so that it is convenient to see the code, while analyzing the logic, the above code logic is long, need to be well analyzed, the more complex the logic, so will slowly progress.

/ ctx.service.bill.list /service/bill.js/ctx.bill.list /service/bill.js/ctx.bill.list/ctx.bill.js/ctx.bill.js/ctx.bill.js/ctx.bill.js/ctx.bill.js/ctx.bill.js/ctx.bill.js

// Get the list of bills
async list(id) {
  const { app } = this;
  const QUERY_STR = 'id, pay_type, amount, date, type_id, type_name, remark';
  let sql = `select ${QUERY_STR} from bill where user_id = ${id}`;
  try {
    return await app.mysql.query(sql);
  } catch (error) {
    console.log(error);
    return null; }}Copy the code

Select * from bill where user_id = current user ID; select * from bill where user_id = current user ID; select * from bill where user_id = current user ID And the returned properties are ID, pay_type, amount, date, type_id, type_name, remark “.

Throw an interface:

// router.js 
router.get('/api/bill/list', _jwt, controller.bill.list); // Get the list of bills
Copy the code

Go to Postman to verify that this works:

Bill modification interface

Continue to make bills to modify the interface, the difference between the modified interface and the new interface is that the new interface is in the absence of the case, edit the parameters, add into the database. The modify interface is to edit the existing data and update the data according to the ID of the current bill.

So there are two interfaces that need to be implemented:

  1. Interface to get bill details
  2. Update data Interface

To complete the interface for getting bill details, add the detail method to /controller/bill.js as follows:

// Get billing details
async detail() {
  const { ctx, app } = this;
  // Get the bill ID parameter
  const { id = ' ' } = ctx.query
  // Get user_id
  let user_id
  const token = ctx.request.header.authorization;
  // Get the current user information
  const decode = app.jwt.verify(token, app.config.jwt.secret);
  if(! decode)return
  user_id = decode.id
  // Determine whether the bill ID is passed in
  if(! id) { ctx.body = {code: 500.msg: 'Order ID cannot be empty'.data: null
    }
    return
  }

  try {
    // Get billing details from the database
    const detail = await ctx.service.bill.detail(id, user_id)
    ctx.body = {
      code: 200.msg: 'Request successful'.data: detail
    }
  } catch (error) {
    ctx.body = {
      code: 500.msg: 'System error'.data: null}}}Copy the code

After writing the above logic, you need to go to /service/bill.js and add the ctx.service.bill.detail method as follows:

// Get billing details
async detail(id, user_id) {
  const { app } = this;
  try {
    return await app.mysql.get('bill', { id, user_id });
  } catch (error) {
    console.log(error);
    return null; }}Copy the code

Throw interface:

router.get('/api/bill/detail', _jwt, controller.bill.detail); // Get details
Copy the code

Open Postman to see if you can get the bill by id:

You can edit and modify the current bill by clicking on the bill list to go to the bill details page.

This leads to the edit bill interface, adding the update method to /controller/bill.js as follows:

// Edit the bill
async update() {
  const { ctx, app } = this;
  // Add the id of the bill
  const { id, amount, type_id, type_name, date, pay_type, remark = ' ' } = ctx.request.body;
  // void processing
  if(! amount || ! type_id || ! type_name || ! date || ! pay_type) { ctx.body = {code: 400.msg: 'Parameter error'.data: null}}try {
    let user_id
    const token = ctx.request.header.authorization;
    const decode = await app.jwt.verify(token, app.config.jwt.secret);
    if(! decode)return
    user_id = decode.id
    // Modify the billing data according to the billing ID and user_id
    const result = await ctx.service.bill.update({
      id,Id / / bill
      amount,/ / the amount
      type_id,// Consumption type ID
      type_name, // Consumption type name
      date,/ / date
      pay_type,// Consumption type
      remark,/ / note
      user_id/ / user id
    });
    ctx.body = {
      code: 200.msg: 'Request successful'.data: null}}catch (error) {
    ctx.body = {
      code: 500.msg: 'System error'.data: null}}}Copy the code

Update ctx.service.bill.update ctx.service.bill.update ctx.service.bill.update ctx.service.bill.update ctx.service.bill.update ctx.service.bill.update ctx.service.bill.update ctx.service.bill.update ctx.service.bill.js

// Edit the bill
async update(params) {
  const { ctx, app } = this;
  try {
    let result = await app.mysql.update('bill', {
      ...params,
    }, {
      id: params.id,
      user_id: params.user_id,
    });
    return result;
  } catch (error) {
    console.log(error);
    return null; }}Copy the code

The app.mysql.update method has been explained in (2).

The first parameter is the name of the database table to operate on bill; The second parameter is the data content to be updated. Expand the parameter directly here. The third query parameter specifies the id and user_id.

When done, throw the interface:

router.post('/api/bill/update', _jwt, controller.bill.update); // Bill updated
Copy the code

Verify interface feasibility with Postman:

Through the details interface, the request is successfully modified:

As expected, the changes took effect.

4. Bill deletion interface

The delete interface is probably the easiest of these interfaces to implement. You only need to obtain the ID of a single bill and delete the corresponding bill data in the database through the ID. Open /controller/bill.js and add the delete method as follows:

// Delete the bill
async delete() {
  const { ctx, app } = this;
  const { id } = ctx.request.body;

  if(! id) { ctx.body = {code: 400.msg: 'Parameter error'.data: null}}try {
    let user_id
    const token = ctx.request.header.authorization;
    const decode = app.jwt.verify(token, app.config.jwt.secret);
    if(! decode)return
    user_id = decode.id
    const result = await ctx.service.bill.delete(id, user_id);
    ctx.body = {
      code: 200.msg: 'Request successful'.data: null}}catch (error) {
    ctx.body = {
      code: 500.msg: 'System error'.data: null}}}Copy the code

And go to /service/bill.js and add the delete service as follows:

// Delete the bill
  async delete(id, user_id) {
    const { app } = this;
    try {
      let result = await app.mysql.delete('bill', {
        / / short
        id,
        user_id,
      });
      return result;
    } catch (error) {
      console.log(error);
      return null; }}}Copy the code

The app.mysql.delete method takes two parameters, the first is the database table name and the second is the query condition. The query criteria given here are bill ID and user user_id. In fact, you can not pass user_id, because the bill ID is increment, there will not be a duplicate value, but for security purposes, user_id is used to double insurance.

Throw an interface:

// router.js
router.post('/api/bill/delete', _jwt, controller.bill.delete); // Delete the bill
Copy the code

The error message is Token has expired, please log in again. This indicates that when the token was generated earlier, the validity of the configuration took effect.

Of course, you can set the expiration date to 1 minute to test whether it works.

Obtain the new token through the login interface again, as shown below:

Delete the interface again using the new token:

The request was successful. The database bill table is empty.

5. Data chart module

After completing the CRUD of the above billing module, you are basically familiar with the process of adding and changing the difference of a table. Learning this thing, a lot of times is to rely on constant practice, even the same thing, you can not do a skilled, practice makes perfect. Therefore, the next step is to process and analyze the data module and make a data chart interface. Before realizing the interface, we should first look at the requirements to be realized:

The first is the summary data for the header, and the interface supports event filtering on a monthly basis.

Next is the composition chart of income and expenditure, which sums up each type of expenditure and income, and finally ranks them from large to small by calculating the proportion. As shown in the figure above, the total learning expenditure for the current month is 2553. This sum is calculated on the server side.

Finally, echarts is introduced to complete a simple pie chart arrangement, which is actually a variation of the above balance of payments chart.

{
  total_data: [{number: 137.84.// Amount of expenditure or income
      pay_type: 1.// Expenditure or consumption type value
      type_id: 1.// Consumption type ID
      type_name: "Food" // Consumption type name}].total_expense: 3123.54./ / total consumption
  total_income: 6555.80 / / total income
}
Copy the code

Data interface implementation

After the above analysis, presumably with already have a plan in mind. Since the data is strongly related to the bill, write the method in /controller/bill.js and add the data method. First, according to the user information, obtain the relevant data of the bill table, as shown below:

async data() {
  const { ctx, app } = this;
  const { date = ' ' } = ctx.query
  // Get user_id
  / /...
  // omit the authentication to obtain user information code
  try {
    // Get the bill data from the bill table
    const result = await ctx.service.bill.list(user_id);
    // Filter out all billing data for the current month according to time parameters
    const start = moment(date).startOf('month').unix() * 1000; // Select month
    const end = moment(date).endOf('month').unix() * 1000; // Select month, end time
    const _data = result.filter(item= > (Number(item.date) > start && Number(item.date) < end))
  } catch{}}Copy the code

The above _data is the filtered basic bill data of the month. Each data was manually added by users before, so there are many similar items. Next, the job is to merge these similar terms.

The array method reduce is more useful than you might think. In some cumulative operations, its advantages are clear. For example, add the amount value of each item in an array and return it to total_expense. You can, of course, declare a variable outside the forEach method and loop over it as follows:

let total_expense = 0

_data.forEach(item= > {
  if (item.pay_type == 1) {
    total_expense += Number(item.amount)
  }
})
Copy the code

Declaring a variable outside, however, doesn’t look pretty. In many cases, you don’t want to declare variables everywhere, and reduce works well because its second argument, a value, can be declared as the initial value of the loop and passed as the first argument arr on each callback function.

The logic for adding total revenue continues as follows:

/ / total income
const total_income = _data.reduce((arr, cur) = > {
  if (cur.pay_type == 2) {
    arr += Number(cur.amount)
  }
  return arr
}, 0)
Copy the code

At this point, you’ve finished summarizing the data. Next, complete the composition of income and expenditure:

// Get the revenue composition
  let total_data = _data.reduce((arr, cur) = > {
    const index = arr.findIndex(item= > item.type_id === cur.type_id)
    if (index === -1) {
      arr.push({
        type_id: cur.type_id,
        type_name: cur.type_name,
        pay_type: cur.pay_type,
        number: Number(cur.amount)
      })
    }
    if (index > -1) {
      arr[index].number += Number(cur.amount)
    }
    return arr
  }, [])

  total_data = total_data.map(item= > {
    item.number = Number(Number(item.number).toFixed(2))
    return item
  })

  ctx.body = {
    code: 200.msg: 'Request successful'.data: {
      total_expense: Number(total_expense).toFixed(2),
      total_income: Number(total_income).toFixed(2),
      total_data: total_data || [],
    }
  }
} catch (error) {
  ctx.body = {
    code: 500.msg: 'System error'.data: null}}Copy the code

After analyzing the callback function of reduce above, the initial value of ARR is an empty array. Enter the callback function logic. Firstly, find whether there are bills of the same type as the current item cur in ARR, such as study, catering, transportation, etc., through findIndex method.

If index is not found, -1 will be returned, indicating that the consumption type of the current cur is not available in ARR. Therefore, a new type of data is added through arr. Push, with the data structure shown above.

If the same consumption type is found, the index value is greater than -1, so find arr[index] and add the amount of the current item to its number attribute to add the same consumption type.

Finally, all the number data is saved as two decimal places, and the data is returned.

Don’t forget to throw the interface:

router.get('/api/bill/data', _jwt, controller.bill.data); // Get data
Copy the code

7. To summarize

This section has learned a complete add, delete, change and check kit, which can be used as your seed kit. If you have new ideas for requirements in the future, you can add new tables and methods according to such a set as the basis for copying. For example, if I want to make a demand for notebook, I can create a new Note table and realize a set of requirements similar to friends circle, with text and pictures, which can be deleted and added.

And through the data chart interface, the data of the database table for the second processing of the review, consolidate the knowledge points before.