Leyou Mall (15) – order service
1, Order settlement page
Page Jump
At the bottom of the shopping cart page, there is a button to settle:
When you click settlement, you should jump to the order settlement page, that is, getOrderInfo.html
To view the settlement button of the shopping cart:
As you can see, the address is correct. However, only the logged in user can settle the payment, so you can't jump directly. Instead, verify the user's login status before jumping. If you find that you are not logged in, you should redirect to the login page!
Bind the click event to this button:
Judge the login status in the event and jump to the page:
toOrderInfo() { // Determine whether to log in ly.verifyUser().then(() => { // Logged in window.location.href = "/getOrderInfo.html" }).catch(() => { // Not logged in window.location.href = "/login.html?returnUrl=" + window.location.href; }) }
Post login test:
The content to be rendered on this page mainly includes three parts:
- Consignee information
- Payment method
- Commodity information
2, Address management
2.1 page effect
Click "add receiving address" and "Edit" to pop up the modal box, as shown below:
2.2 database design
CREATE TABLE `tb_address` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'address id', `user_id` bigint(20) NULL DEFAULT NULL COMMENT 'user id', `name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'Consignee Name ', `phone` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'Consignee telephone', `zip_code` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'Zip code', `state` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'province', `city` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'city', `district` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'area/county', `address` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'Detailed address', `default_address` tinyint(1) NULL DEFAULT NULL COMMENT '1: Default address 0: non default address', `label` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'Address label', PRIMARY KEY (`id`) USING BTREE, INDEX `userId`(`user_id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
2.3 page rendering
2.3.1 define data model
2.3.2 form modification
<div tabindex="-1" role="dialog" data-hasfoot="false" class="sui-modal hide fade edit"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <button type="button" @click="clear" data-dismiss="modal" aria-hidden="true" class="sui-close">×</button> <h4 id="myModalLabel" class="modal-title">{{isEdit ? "edit" : "add to"}}Receiving address</h4> </div> <div class="modal-body"> <form id="myform" action="" class="sui-form form-horizontal"> <div class="control-group"> <label class="control-label">consignee:</label> <div class="controls"> <input type="text" v-model="addressForm.name" class="input-medium"> </div> </div> <div class="control-group"> <label class="control-label">contact number:</label> <div class="controls"> <input type="text" v-model="addressForm.phone" class="input-medium"> </div> </div> <div class="control-group"> <label class="control-label">Province:</label> <div class="controls"> <input type="text" v-model="addressForm.state" class="input-medium"> </div> </div> <div class="control-group"> <label class="control-label">City:</label> <div class="controls"> <input type="text" v-model="addressForm.city" class="input-medium"> </div> </div> <div class="control-group"> <label class="control-label">area/County:</label> <div class="controls"> <input type="text" v-model="addressForm.district" class="input-medium"> </div> </div> <div class="control-group"> <label class="control-label">Zip code:</label> <div class="controls"> <input type="text" v-model="addressForm.zipCode" class="input-medium"> </div> </div> <div class="control-group"> <label class="control-label">Detailed address:</label> <div class="controls"> <input type="text" v-model="addressForm.address" class="input-large"> </div> </div> <div class="control-group"> <label class="control-label">Address label:</label> <div class="controls"> <select class="select" v-model="addressForm.label"> <option value="home">home</option> <option value="company">company</option> <option value="school">school</option> </select> </div> </div> <div class="control-group"> <div style="margin-left: 100px"> <input type="checkbox" v-model="addressForm.defaultAddress" class="checkbox">Set as default shipping address </div> </div> </form> </div> <div class="modal-footer"> <button type="button" @click="addressSave" data-ok="modal" class="sui-btn btn-primary btn-large">determine</button> <button type="button" @click="clear" data-dismiss="modal" class="sui-btn btn-default btn-large">cancel</button> </div> </div> </div> </div>
2.3.3 method binding
New address
Modify address
Submit and cancel forms
There are two places to cancel, one is cancel and the other is X
2.4 back end implementation
2.4.1 entity class
@Table(name = "tb_address") public class Address { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @NotNull private Long userId; //User id private String name; //Consignee name private String phone; //Receiving mobile phone private String zipCode; //Zip code private String state; //province private String city; //city private String district; //District / county private String address; //Specific address private Boolean defaultAddress; //Is it the default address private String label; //label //get and set }
2.4.2,AddressMapper
/** * Address Generic mapper for */ public interface AddressMapper extends Mapper<Address> { }
2.4.3,AddressController
Basic addition, deletion, modification and query
@RestController @Api("Address management interface") @RequestMapping("/address") public class AddressController { @Autowired private AddressService addressService; /** * Add receiving address * @param address * @return */ @PostMapping @ApiOperation(value = "Add a receiving address of the user",notes = "Create shipping address") @ApiImplicitParam(name = "address",required = true,value = "Receiving address object") @ApiResponses({ @ApiResponse(code = 201,message = "Address added successfully"), @ApiResponse(code = 500,message = "Server exception") }) public ResponseEntity<Void> addAddress(@RequestBody Address address){ this.addressService.addAddress(address); return ResponseEntity.status(HttpStatus.CREATED).build(); } /** * Query all receiving addresses according to user id * @return */ @GetMapping @ApiOperation(value = "Query all receiving addresses under the current login user",notes = "Query receiving address") @ApiResponses({ @ApiResponse(code = 200,message = "All receiving addresses of the user were queried successfully"), @ApiResponse(code = 404,message = "The receiving address of the user is not found"), @ApiResponse(code = 500,message = "Server exception") }) public ResponseEntity<List<Address>> queryAddressByUserId(){ List<Address> addresses = this.addressService.queryAddressByUserId(); if (CollectionUtils.isEmpty(addresses)){ return ResponseEntity.notFound().build(); } return ResponseEntity.ok(addresses); } /** * Update shipping address based on user id * @param address * @return */ @PutMapping @ApiOperation(value = "Update the receiving address of the current user",notes = "Update shipping address") @ApiImplicitParam(name = "address",required = true,value = "Receiving address object") @ApiResponses({ @ApiResponse(code = 204,message = "Update of receiving address succeeded"), @ApiResponse(code = 500,message = "Server exception") }) public ResponseEntity<Void> updateAddressByUserId(@RequestBody Address address){ this.addressService.updateAddress(address); return ResponseEntity.noContent().build(); } /** * Query the receiving address according to the address id * @param addressId * @return */ @GetMapping("{addressId}") @ApiOperation(value = "Query a receiving address",notes = "Query receiving address") @ApiImplicitParam(name = "addressId",value = "Receiving address No",required = true) @ApiResponses({ @ApiResponse(code = 200,message = "query was successful"), @ApiResponse(code = 404,message = "Address not found"), @ApiResponse(code = 500,message = "Server exception") }) public ResponseEntity<Address> queryAddressById(@PathVariable("addressId") Long addressId){ Address address = this.addressService.queryAddressById(addressId); if (null == address){ return ResponseEntity.notFound().build(); } return ResponseEntity.ok(address); } /** * Delete a shipping address * @param addressId * @return */ @DeleteMapping("{addressId}") @ApiOperation(value = "Delete a shipping address",notes = "Delete shipping address") @ApiImplicitParam(name = "addressId",value = "Receiving address No",required = true) @ApiResponses({ @ApiResponse(code = 200,message = "Delete succeeded"), @ApiResponse(code = 500,message = "Deletion failed") }) public ResponseEntity<Void> deleteAddress(@PathVariable("addressId") Long addressId){ this.addressService.deleteAddress(addressId); return ResponseEntity.ok().build(); } }
2.4.4,AddressService
public interface AddressService { /** * Add receiving address * @param address * @return */ void addAddress(Address address); /** * Query all receiving addresses according to user id * @return */ List<Address> queryAddressByUserId(); /** * Update shipping address based on user id * @param address * @return */ void updateAddress(Address address); /** * Query the receiving address according to the address id * @param addressId * @return */ Address queryAddressById(Long addressId); /** * Delete a shipping address * @param addressId * @return */ void deleteAddress(Long addressId); }
Implementation class:
@Service public class AddressServiceImpl implements AddressService { @Autowired private AddressMapper addressMapper; /** * Add receiving address * * @param address * @return */ @Override public void addAddress(Address address) { UserInfo userInfo = LoginInterceptor.getLoginUser(); address.setUserId(userInfo.getId()); setDefaultAddress(address); this.addressMapper.insert(address); } /** * Query all receiving addresses according to user id * * @return */ @Override public List<Address> queryAddressByUserId() { UserInfo userInfo = LoginInterceptor.getLoginUser(); Example example = new Example(Address.class); example.createCriteria().andEqualTo("userId",userInfo.getId()); return this.addressMapper.selectByExample(example); } /** * Update shipping address based on user id * * @param address * @return */ @Override public void updateAddress(Address address) { UserInfo userInfo = LoginInterceptor.getLoginUser(); address.setUserId(userInfo.getId()); setDefaultAddress(address); this.addressMapper.updateByPrimaryKeySelective(address); } /** * Query the receiving address according to the address id * * @param addressId * @return */ @Override public Address queryAddressById(Long addressId) { //You also need to match the user id when querying UserInfo userInfo = LoginInterceptor.getLoginUser(); Example example = new Example(Address.class); example.createCriteria().andEqualTo("id",addressId).andEqualTo("userId",userInfo.getId()); return this.addressMapper.selectByExample(example).get(0); } /** * Delete a shipping address * * @param addressId * @return */ @Override public void deleteAddress(Long addressId) { UserInfo userInfo = LoginInterceptor.getLoginUser(); Example example = new Example(Address.class); example.createCriteria().andEqualTo("id",addressId).andEqualTo("userId",userInfo.getId()); this.addressMapper.deleteByExample(example); } /** * Set the default state of the previous address to false * @param address */ private void setDefaultAddress(Address address){ if (address.getDefaultAddress()){ List<Address> addresses = queryAddressByUserId(); //If this address is set as the default address, other addresses under this user should be non default addresses addresses.forEach(addr ->{ if (addr.getDefaultAddress()){ addr.setDefaultAddress(false); this.addressMapper.updateByPrimaryKeySelective(addr); } }); } } }
2.4.5 Gateway Routing
2.5 interface test
cookie information needs to be set during interface test
2.5.1. New address
Data entry:
{ "name":"Li Si", "phone":"13555555555", "zipCode":"123123", "state":"Sichuan Province", "city":"Chengdu", "district":"High tech Zone", "address":"Chengdu hi tech Zone, Sichuan Province", "label":"company", "defaultAddress":true }
result
View database:
2.5.2. Query all addresses
result
Currently, the user has only one address
2.5.3 address modification
Data entry:
{ "id":"4", "name":"Li Si modified", "phone":"13555555555", "zipCode":"123123", "state":"Sichuan Province", "city":"Chengdu", "district":"High tech Zone", "address":"Chengdu hi tech Zone, Sichuan Province", "label":"Company modification", "defaultAddress":true }
result
Database:
2.5.4. Query an address
result
2.5.5 delete an address
result
Query the address of the current user through the query all addresses interface:
2.6 page transformation
2.6.1 address query
When the page is loaded, query all address information under the current login user. Then save it in the optional address list addresses for rendering. When querying the address, put the default address first:
Page rendering
effect
Default address:
A user can only set one default address, so it needs to be handled when adding and modifying addresses. If the new or modified address is set as the default address, other addresses of the user will become non default addresses
2.6.2 add address
addressSave(){ //1. Verify whether to log in ly.verifyUser().then(() => { //2. Log in, make a request, save or modify the address if (this.isEdit === false) { //2.1 NEW ly.http.post("/address", this.addressForm).then(() => { //Save successfully, reload data this.loadData(); //Empty form this.clear(); }).catch() }else { //2.2 modification ly.http.put("/address", this.addressForm).then(() => { //Modification succeeded. Reload the data this.loadData(); //Empty form this.clear(); }).catch() } }).catch(() => { //3. Not logged in window.location.href = "/login.html?returnUrl=" + window.location.href; }); }
Here, add and modify requests are combined, and a data model is used to mark whether to modify or add:
2.6.3 data echo
When you click Modify, query the corresponding address information through the passed in address id, and then echo it to the form
editAddress(id){ this.isEdit = true; ly.verifyUser().then(() => { ly.http.get("/address/"+id).then(({data}) => { this.addressForm = data; }) }).catch(() => { window.location = "/login.html?returnUrl=" + window.location.href; }) }
2.6.4 address deletion
deleteAddress(id){ ly.verifyUser().then(() => { ly.http.delete("/address/"+id).then(() => { this.loadData(); }) }).catch(() => { window.location.href = "/login.html?returnUrl=" + window.location.href; }); }
3, Payment method
There are two payment methods:
- Wechat payment
- Cash on Delivery
Associated with paymentType in order data:
Therefore, a data model is defined in the Vue instance to record the payment method:
It is then associated with this variable when the page is rendered:
4, Commodity information
design sketch
The delivery list here is actually the goods to be paid selected by the user in the shopping cart
Therefore, it is necessary to carry the information of the selected shopping cart while the shopping cart jumps over
4.1. Shopping cart information acquisition
Modify the page Jump logic in cart.html to transfer the shopping cart information selected by the user:
Then obtain the shopping cart data in the created hook function and save it to the local attribute. Note that the user login status should be verified before obtaining the data. If it is found that the user is not logged in, it will be redirected to the login page directly:
Then reload the page to view the console:
4.2 page rendering
4.3 total amount
It can be seen that there are four main data:
- Total amount: totalPay
- Discount cash back: discount
- Freight: postFee
- Paid in amount: actualPay
However, there is no preferential activity module at present. In addition, the freight needs to be calculated in combination with the logistics system. For the time being, it is not available and set to 0. Write it in the order attribute:
totalPay and actualPay values are obtained by calculating attributes:
computed:{ totalNum(){ return this.carts.reduce((c1,c2) => c1 + c2.num,0); }, totalPay(){ return this.carts.reduce((c1,c2) => c1 + c2.price * c2.num,0); }, actualPay(){ return this.totalPay + this.order.postFee - this.order.discount; } }
Then render on the page:
There is also a display of the receiving address, which is rendered together
4.4. Submit order
4.4.1 page submission
See the data required for the order interface:
{ "totalPay": 236800, "postFee": 0, "paymentType": 2, "actualPay": 236800, "buyerMessage": null, "buyerNick": "huge", "orderDetails": [ { "skuId": 3893493, "num": 1, "title": "Apples( Apple)iPhone 6 (A1586) 16GB Golden Mobile Unicom Telecom 4 G Mobile 3", "price": 236800, "ownSpec": "{\"body color \":\"Diamond carving blue\",\"Memory\":\"4GB\",\"Fuselage storage\":\"64GB\"}", "image": "http://image.leyou.com/images/9/4/1524297342728.jpg" } ], "receiver": "Account cancellation", "receiverMobile": "15800000000", "receiverState": "Shanghai", "receiverCity": "Shanghai", "receiverDistrict": "Pudong New Signature", "receiverAddress": "Building 3, dump truck, No. 18, hangtou Road, hangtou town", "receiverZip": "210000", "invoiceType": 0, "sourceType":2 }
It is divided into three parts
-
Basic information of the order itself
- Total amount
- Paid in amount
- Payment type
- Buyer information is the current user
-
Order details
-
It is the goods in the shopping cart, but the shopping cart data will have an additional userId, which can be removed:
-
-
Logistics information
- Logistics address information selected by the current user
Binding event:
Note: @ click.prevent is used to prevent the default jump of a tag and execute the submit function.
method
methods: { submit() { // Process shopping cart data into order details const orderDetails = this.carts.map(({userId, ...rest}) => rest); // Processing logistics information const addr = this.addresses[this.selectedAddress]; const obj = { receiver: addr.name, receiverState: addr.state, receiverCity: addr.city, receiverAddress: addr.address, receiverDistrict: addr.district, receiverMobile: addr.phone, receiverZip: addr.zipCode }; // Copy to order object Object.assign(this.order, obj, { orderDetails, totalPay: this.totalPay, actualPay: this.actualPay, }); // place order ly.http.post("/order", this.order).then(({data}) => { // Online payment, need to go to the payment page window.location = "pay.html?orderId=" + data; }).catch((resp) => { alert("Order submission failed. Please try again later!") }) } },
4.4.2 accuracy loss
Click submit test on the page:
Order id in database:
But look carefully at the order id carried on the page:
As mentioned earlier, there will be precision loss. What is the reason?
This is actually because JS's Long integer precision is limited, and java's Long type data is out of range, so there is a precision loss.
JSON's string is returned in the background. JSON.parse() method will be automatically called inside axios to convert JSON string into JS data, and progress loss will occur. If you do not convert and still use it as a string, there will be no problem.
Of course, this is only one of the solutions. The front-end solution is not elegant enough
Back end solution:
Jackson is used by default in Spring MVC. Solved by rewriting the converter
@Configuration @EnableWebMvc //You need to use swagger to test, and you need to annotate this annotation temporarily public class MvcConfig implements WebMvcConfigurer { @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter(); ObjectMapper objectMapper = new ObjectMapper(); SimpleModule simpleModule = new SimpleModule(); simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance); simpleModule.addSerializer(Long.class, ToStringSerializer.instance); simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance); objectMapper.registerModule(simpleModule); jackson2HttpMessageConverter.setObjectMapper(objectMapper); converters.add(jackson2HttpMessageConverter); converters.add(new StringHttpMessageConverter(Charset.forName("UTF-8"))); } }
5, Wechat payment
5.1 introduction
Official wechat payment documents: https://pay.weixin.qq.com/index.php/core/home/login?return_url=%2F
Select the document center and click API document
Select Native payment in basic payment and click development guide
5.2 development process
Business Process Description:
- The merchant background system generates orders according to the commodities purchased by users.
- Users confirm payment and call WeChat payment (unified order API) to generate pre paid transactions.
- After receiving the request, the wechat payment system generates an advance payment transaction form and returns the QR code link code of the transaction session_ url.
- The merchant background system returns the code according to the_ The URL generates a QR code.
- The user opens the wechat "scan" to scan the QR code, and the wechat client sends the scanned content to the wechat payment system.
- The wechat payment system receives the client request and initiates user payment after verifying the validity of the link, requiring user authorization.
- After the user enters the password in the wechat client and confirms the payment, the wechat client submits the authorization.
- The wechat payment system completes the payment transaction according to the user's authorization.
- After completing the payment transaction, the wechat payment system returns the transaction results to the wechat client, and prompts the user of the transaction results through SMS and wechat messages. Wechat client displays the payment transaction result page.
- The wechat payment system notifies the merchant background system of the payment results by sending asynchronous messages. The merchant background system needs to reply to the reception and notify the wechat background system not to send the payment notice of this order.
- If the payment notice is not received, the merchant background system calls [query order API].
- The merchant will deliver the goods to the user after confirming that the order has been paid.
concrete work
Here is a summary of what the merchant should do:
-
Merchant generates order
-
The merchant calls the wechat order interface to obtain the link of pre transaction
-
The merchant will link to generate a QR code picture and display it to the user;
-
User pays and confirms
-
Payment result notice:
- Wechat asynchronously informs the merchant of the payment results, and the merchant informs the wechat payment reception
- If the merchant does not receive the notification, it can call the interface to query the payment status
-
If the payment is successful, the order will be shipped and the order status will be modified
In the previous business, we have completed:
- Generate order
What needs to be done next is:
-
Call wechat interface to generate links.
-
Generate payment QR code picture
5.3. Generate QR code
5.3.1. Generate pre transaction link
First call the background service according to the order number to generate a transaction link, and then generate a QR code according to the link
Initiate a request on the page:
var payVm = new Vue({ el:"#payVm", data:{ ly, orderId:0, //Order id }, components:{ shortcut: () => import("/js/pages/shortcut.js") }, created(){ this.loadData(); }, methods:{ loadData(){ //1. Judge whether the user logs in ly.verifyUser().then(() => { //2. Get order number this.orderId = ly.getUrlParam("orderId"); //3. Get request link ly.http.get("/order/url/" + this.orderId).then(resp => { console.log(resp.data); new QRCode(document.getElementById("qrImage"),{ text:resp.data, width:250, height:250, colorDark: "#000000", colorLight: "#ffffff", correctLevel: QRCode.CorrectLevel.H }); }); }).catch(() => { //4. If you are not logged in, go to the login page location.href = "/login.html?returnUrl=" + location.href; }) } } });
Note: app.js should be introduced last, because the div with id app must be available before Vue can get the corresponding element
The interface for generating payment address has been defined in the background: generateUrl
5.3.2. Generate QR code
Use the JS plug-in generating QR Code: qrcode, official website: https://github.com/davidshimjs/qrcodejs
Bring the js into the project
Page introduction
The page defines a div to display the QR code
QR code related styles
5.4. Payment status query
However, because it is not clear when the user will pay, a circular method is adopted here to constantly request to judge whether the payment is successful
Payment successful
6, Page optimization
6.1. The payment page displays the total amount
getOrderInfo.html
When the order is successfully submitted and the order number is returned, the calculated total amount is stored in LocalStorage as the page jumps to the payment page, and then displayed where necessary
pay.html
When loading data, get the total amount:
Page rendering:
paysuccess.html
When loading the payment success page, first authenticate the user, then read the total amount and delete the local storage
Page rendering:
6.2. Modify the transfer method of order number
Put the order number into the local storage and no longer pass it through the access path.
Payment is successful. Delete the order number in LocalStorage.
If payment fails, you can also jump to the payment page to continue the payment operation
Modify order submission function
Get the order number from the payment page
After the payment is successful, delete the locally stored order number
6.3. Shopping cart data update
After successful payment, delete the data stored locally (paysuccess.html)
After the user pays successfully, the paid goods will be deleted from the shopping cart.
New interface: query skuId by order id
controller
- Request method: GET
- Request path: / order/skuId/{id}
- Request parameter: id, order id
- Return result: item number collection
/** * Query all skuids under the order according to the order id * @param orderId * @return */ @GetMapping("/skuId/{id}") @ApiOperation(value = "Query all commodity numbers under the order according to the order number",notes = "Query commodity number") @ApiImplicitParam(name = "id",value = "Order No",type = "Long") @ApiResponses({ @ApiResponse(code = 200,message = "Item order number collection"), @ApiResponse(code = 404,message = "The item under this order number was not found"), @ApiResponse(code = 500,message = "Server exception") }) public ResponseEntity<List<Long>> querySkuIdByOrderId(@PathVariable("id") Long orderId){ List<Long> skuIds = this.orderService.querySkuIdByOrderId(orderId); if (CollectionUtils.isEmpty(skuIds)){ return ResponseEntity.notFound().build(); } return ResponseEntity.ok(skuIds); }
Service
/** * Query all skuids under the order according to the order id * @param orderId * @return */ List<Long> querySkuIdByOrderId(Long orderId);
Implementation class:
/** * Query all skuids under the order according to the order id * * @param orderId * @return */ @Override public List<Long> querySkuIdByOrderId(Long orderId) { Example example = new Example(OrderDetail.class); example.createCriteria().andEqualTo(orderId); List<OrderDetail> orderDetails = this.orderDetailMapper.selectByExample(example); //Return item collection List<Long> skuIds = new ArrayList<>(); orderDetails.forEach(orderDetail -> skuIds.add(orderDetail.getSkuId())); return skuIds; }
Front end page processing