什么是礼品网
采取礼品代发的方法,可以让店铺用比较低的成本,拥有真实的发货物流,而且不需要自己去处理发货。那么靠谱的礼品代发平台有什么特点?选择礼品代发平台应该看什么?
礼品代发的意义。
当你有一个礼品需要寄给买家,你起码需要以下几个步骤:
虽说整个流程没有技术难度,大家每天也在做这个事情,但如果每天需要发10个,甚至100个礼品单,就会消耗大量的人工和时间成本,算上礼品和快递费的成本,长此以往也是笔不小的开销,为了控制成本留下更多的利润,需要以更低的成本,更高的效率,更便捷的方式去发礼品单。
如何搭建礼品网?怎么搭建一个礼品网?
礼品网分别,用户端,站点可以进行商品的上架,下架管理等操作,支持一键对接第三方云仓库,一键发货
开发工具:
前端: vue
服务端:springboot + mysql + mybaits
源码展示
验证码获取
package cn.gift.web.service.system;
import cn.gift.web.constants.BusinessException;
import cn.gift.web.constants.RedisKeyConstants;
import cn.plus.core.exception.ServiceException;
import cn.plus.core.utils.SerialNumber;
import com.google.code.kaptcha.Producer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @Description: 校准需要的内容
* @author: RaveyXie
* @date: 2022年11月13日 7:59 PM
* @since
*/
@Service
@Slf4j
@RequiredArgsConstructor
public class CheckVerifyCodeService {
private final StringRedisTemplate masterRedisTemplate;
private final Producer producer;
/**
* 校准验证码
*
* @param key
* @param code
* @param isRemoveKey 判断是否删除验证码 用来做发送短信就要校验验证码
*/
public void checkVerifyCode(String key, String code, Boolean isRemoveKey) {
// 校准验证码
if (StringUtils.isEmpty(code)) {
throw new ServiceException(BusinessException.CODE_CANNOT_EMPTY);
}
String redisCode = RedisKeyConstants.KAPTCHA.setArg(key);
String oldCode = masterRedisTemplate.opsForValue().get(redisCode);
if (isRemoveKey) {
masterRedisTemplate.delete(redisCode);
}
if (StringUtils.isEmpty(oldCode)) {
throw new ServiceException(BusinessException.VERIFICATION_CODE_HAS_EXPIRED);
}
if (oldCode.compareToIgnoreCase(code) != 0) {
throw new ServiceException(BusinessException.VERIFICATION_CODE_INPUT_ERROR);
}
}
/**
* 生成验证码
*
* @return
*/
public Object doKaptcha() {
// 生成验证码
String text = producer.createText();
BufferedImage image = producer.createImage(text);
// 将验证码的信息存在session中进行比对
Long serialNum7 = SerialNumber.key7();
String key = RedisKeyConstants.KAPTCHA.setArg(serialNum7.toString());
// 存在redis里面 设置10分钟过期
masterRedisTemplate.opsForValue().set(key, text, 3, TimeUnit.MINUTES);
// 这一步就要存session
Map<String, Object> maps = new HashMap<>(16);
maps.put("key", serialNum7);
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ImageIO.write(image, "jpeg", outputStream);
String base64 = Base64.encodeBase64String(outputStream.toByteArray());
String captchaBase64 = "data:image/jpeg;base64," + base64.replaceAll("\r\n", "");
maps.put("value", captchaBase64);
return maps;
} catch (IOException e) {
return null;
}
}
}
关于密码加密
package cn.gift.web.util;
import com.google.common.collect.Lists;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.tomcat.util.codec.binary.Base64;
import javax.crypto.Cipher;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.*;
/**
* 进行rsa加密 像 RSA 这样的非对称密码旨在加密短数据,通常是对称 key ,而大数据使用对称分组密码加密(对称 key 将与非对称密码交换)。
* StackOverflow 上确实有很多类似的问题和答案。 This one是一个提供了很好的答案。
* 即使这样确实可以解决大数据量加密的问题,但是前端输入的长度是不可控制的 所以我们这边就进行一个操作
* 对大数据量的接口进行过滤。对文件类型接口进行过滤不进行加密
* 1.前端加密规则,因为存在中文所以前端加密会导致后端乱码所以加密规则定下
* - 先进行参数的base64编码
* - 在进行rsa加密
* - 在进行base64编码
* 我们进行长数据的接口进行过滤
*
* @author raveyxie
*/
public class RSAUtils {
protected static final Log log = LogFactory.getLog(RSAUtils.class);
private static String KEY_RSA_TYPE = "RSA";
private static String KEY_RSA_TYPE_ALL = "RSA/ECB/PKCS1Padding";
private static int KEY_SIZE = 1024;
private static int ENCODE_PART_SIZE = KEY_SIZE / 8;
public static final String PUBLIC_KEY_NAME = "public";
public static final String PRIVATE_KEY_NAME = "private";
/**
* 创建公钥秘钥
*
* @return
*/
public static Map<String, String> createRSAKeys() {
//里面存放公私秘钥的Base64位加密
Map<String, String> keyPairMap = new HashMap<>();
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KEY_RSA_TYPE);
keyPairGenerator.initialize(KEY_SIZE, new SecureRandom());
KeyPair keyPair = keyPairGenerator.generateKeyPair();
//获取公钥秘钥
String publicKeyValue = Base64.encodeBase64String(keyPair.getPublic().getEncoded());
String privateKeyValue = Base64.encodeBase64String(keyPair.getPrivate().getEncoded());
//存入公钥秘钥,以便以后获取
keyPairMap.put(PUBLIC_KEY_NAME, publicKeyValue);
keyPairMap.put(PRIVATE_KEY_NAME, privateKeyValue);
} catch (NoSuchAlgorithmException e) {
log.error("当前JDK版本没找到RSA加密算法!");
e.printStackTrace();
}
return keyPairMap;
}
/**
* 公钥加密
* 描述:
* 1字节 = 8位;
* 最大加密长度如 1024位私钥时,最大加密长度为 128-11 = 117字节,不管多长数据,加密出来都是 128 字节长度。
*
* @param sourceStr
* @param publicKeyBase64Str
* @return
*/
public static String encode(String sourceStr, String publicKeyBase64Str) {
byte[] publicBytes = Base64.decodeBase64(publicKeyBase64Str);
//公钥加密
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicBytes);
List<byte[]> alreadyEncodeListData = new LinkedList<>();
int maxEncodeSize = ENCODE_PART_SIZE - 11;
String encodeBase64Result = null;
try {
KeyFactory keyFactory = KeyFactory.getInstance(KEY_RSA_TYPE);
PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec);
Cipher cipher = Cipher.getInstance(KEY_RSA_TYPE_ALL);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] sourceBytes = sourceStr.getBytes(StandardCharsets.UTF_8);
int sourceLen = sourceBytes.length;
for (int i = 0; i < sourceLen; i += maxEncodeSize) {
int curPosition = sourceLen - i;
int tempLen = curPosition;
if (curPosition > maxEncodeSize) {
tempLen = maxEncodeSize;
}
//待加密分段数据
byte[] tempBytes = new byte[tempLen];
System.arraycopy(sourceBytes, i, tempBytes, 0, tempLen);
byte[] tempAlreadyEncodeData = cipher.doFinal(tempBytes);
alreadyEncodeListData.add(tempAlreadyEncodeData);
}
//加密次数
int partLen = alreadyEncodeListData.size();
int allEncodeLen = partLen * ENCODE_PART_SIZE;
//存放所有RSA分段加密数据
byte[] encodeData = new byte[allEncodeLen];
for (int i = 0; i < partLen; i++) {
byte[] tempByteList = alreadyEncodeListData.get(i);
System.arraycopy(tempByteList, 0, encodeData, i * ENCODE_PART_SIZE, ENCODE_PART_SIZE);
}
encodeBase64Result = Base64.encodeBase64String(encodeData);
} catch (Exception e) {
e.printStackTrace();
}
return encodeBase64Result;
}
/**
* 私钥解密
* 1.前端加密规则,因为存在中文所以前端加密会导致后端乱码所以加密规则定下
* - 先进行参数的base64编码
* - 在进行rsa加密
* - 在进行base64编码
*
* @param sourceBase64RSA
* @param privateKeyBase64Str
*/
public static String decode(String sourceBase64RSA, String privateKeyBase64Str) {
byte[] privateBytes = Base64.decodeBase64(privateKeyBase64Str);
byte[] encodeSource = Base64.decodeBase64(sourceBase64RSA);
int encodePartLen = encodeSource.length / ENCODE_PART_SIZE;
//所有解密数据
List<byte[]> decodeListData = new LinkedList<>();
String decodeStrResult = null;
//私钥解密
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateBytes);
try {
KeyFactory keyFactory = KeyFactory.getInstance(KEY_RSA_TYPE);
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
Cipher cipher = Cipher.getInstance(KEY_RSA_TYPE_ALL);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
//初始化所有被解密数据长度
int allDecodeByteLen = 0;
for (int i = 0; i < encodePartLen; i++) {
byte[] tempEncodedData = new byte[ENCODE_PART_SIZE];
System.arraycopy(encodeSource, i * ENCODE_PART_SIZE, tempEncodedData, 0, ENCODE_PART_SIZE);
byte[] decodePartData = cipher.doFinal(tempEncodedData);
decodeListData.add(decodePartData);
allDecodeByteLen += decodePartData.length;
}
byte[] decodeResultBytes = new byte[allDecodeByteLen];
for (int i = 0, curPosition = 0; i < encodePartLen; i++) {
byte[] tempSorceBytes = decodeListData.get(i);
int tempSourceBytesLen = tempSorceBytes.length;
System.arraycopy(tempSorceBytes, 0, decodeResultBytes, curPosition, tempSourceBytesLen);
curPosition += tempSourceBytesLen;
}
decodeStrResult = new String(decodeResultBytes, StandardCharsets.UTF_8);
} catch (Exception e) {
e.printStackTrace();
}
return new String(Base64.decodeBase64(decodeStrResult));
}
/**
* 过滤链接
*/
public static final List<String> NO_RSA_URI = Lists.newArrayList(
"/business/basic/cutAddress",
"/business/order/saveOrder",
"/business/basic/templateImport");
/**
* 获取过滤链接
*
* @param webFirst
* @return
*/
public static List<String> getFilterUrlInfo(String webFirst) {
List<String> webFilter = new ArrayList<>();
RSAUtils.NO_RSA_URI.forEach(item -> webFilter.add(webFirst + item));
return webFilter;
}
}
加密算法
package cn.gift.web.util;
/*
* Copyright 2002-2011 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import lombok.extern.slf4j.Slf4j;
import java.security.SecureRandom;
import java.util.regex.Pattern;
/**
* Implementation of PasswordEncoder that uses the BCrypt strong hashing function. Clients
* can optionally supply a "strength" (a.k.a. log rounds in BCrypt) and a SecureRandom
* instance. The larger the strength parameter the more work will have to be done
* (exponentially) to hash the passwords. The default value is 10.
* 进行密码的加密
* @author Dave Syer
*/
@Slf4j
public class BCryptPasswordEncoder {
private final int strength;
private final SecureRandom random;
private Pattern BCRYPT_PATTERN = Pattern
.compile("\\A\\$2a?\\$\\d\\d\\$[./0-9A-Za-z]{53}");
public BCryptPasswordEncoder() {
this(-1);
}
/**
* @param strength the log rounds to use, between 4 and 31
*/
public BCryptPasswordEncoder(int strength) {
this(strength, null);
}
/**
* @param strength the log rounds to use, between 4 and 31
* @param random the secure random instance to use
*/
public BCryptPasswordEncoder(int strength, SecureRandom random) {
if (strength != -1 && (strength < BCrypt.MIN_LOG_ROUNDS || strength > BCrypt.MAX_LOG_ROUNDS)) {
throw new IllegalArgumentException("Bad strength");
}
this.strength = strength;
this.random = random;
}
public String encode(CharSequence rawPassword) {
String salt;
if (strength > 0) {
if (random != null) {
salt = BCrypt.gensalt(strength, random);
} else {
salt = BCrypt.gensalt(strength);
}
} else {
salt = BCrypt.gensalt();
}
return BCrypt.hashpw(rawPassword.toString(), salt);
}
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (encodedPassword == null || encodedPassword.length() == 0) {
log.warn("Empty encoded password");
return false;
}
if (!BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
log.warn("Encoded password does not look like BCrypt");
return false;
}
return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
}
}
前端代码
<template>
<div id="wxLogin">
<div v-if="type == -1" class="form">
<img :src="$store.state.tenantInfo.logo | fullPath" />
<p style="font-size: 18px">商家端</p>
<input type="tel" placeholder="用户名" v-model="formData.username" />
<input type="password" placeholder="密码" v-model="formData.password" />
<div class="verification">
<input type="text" placeholder="验证码" v-model="formData.verifyCode" />
<div @click="reflashCode" id="imgcode">
<img :src="imgUrl" />
</div>
</div>
<input @click="login" type="button" value="登录" />
</div>
<div v-else-if="type == 1" class="form">
<img :src="$store.state.tenantInfo.logo | fullPath" />
<h3>请点击下方按钮授权</h3>
<p>授权后,你将开通微信提现功能,提现时金额将到此微信</p>
<input
id="authorization"
@click="updateOpenid"
type="button"
value="一键授权"
/>
</div>
<div v-else-if="type == 2" class="form">
<img :src="$store.state.tenantInfo.logo | fullPath" />
<p>授权成功,你已开通微信提现功能</p>
</div>
<div v-else-if="type == 3" class="form">
<img :src="$store.state.tenantInfo.logo | fullPath" />
<p>
分站已开通微信提现功能,授权的微信账号与当前账号不一致,是否重新授权
</p>
<input
id="authorization"
@click="updateOpenid"
type="button"
value="重新授权"
/>
</div>
</div>
</template>
<script>
import rsa from "@/utils/rsa";
export default {
name: "",
data() {
return {
type: -1, //1未授权,2已授权(授权openid与当前openid一致),3已售罄(授权openid与当前openid不一致)
formData: {
username: "",
password: "",
verifyCode: "",
verifySign: "",
},
imgUrl: "", //图片验证码路径
code: "",
openid: "", //当前openid
};
},
created() {
this.reflashCode();
var openid = this.$route.query.openid;
if (openid) {
this.openid = openid;
}
var token = localStorage.getItem(`dftToken`);
if (openid && token) {
this.decideOpenid();
}
},
mounted() {},
watch: {},
methods: {
//-----------获取图片验证码------------
reflashCode: function () {
var _this = this;
_this.$axios
.get("/ReportApi/basic/system/getVerifyCode", { responseType: "blob" })
.then(function (res) {
_this.imgUrl = window.URL.createObjectURL(res.data);
_this.formData.verifySign = res.headers.sign;
})
.catch(function (error) {
//consloe.log(error);
});
},
//-------登录--------
login: function () {
var _this = this;
if (_this.formData.username == "") {
_this.$message.warning("账户名不能为空");
return;
}
if (_this.formData.password == "") {
_this.$message.warning("密码不能为空");
return;
}
if (_this.formData.verifyCode == "") {
_this.$message.warning("验证码不能为空");
return;
}
// let postData = _this.$qs.stringify(_this.formData);
_this.$request.post({
url: "bus/info/public/authority",
params: {
account: rsa.encryptByPublicKey(_this.formData.username),
password: rsa.encryptByPublicKey(_this.formData.password),
verifyCode: _this.formData.verifyCode,
verifySign: _this.formData.verifySign,
},
success: (res) => {
_this.type = 1;
localStorage.setItem("dftToken", res.token);
localStorage.setItem("phone", res.info.phone);
this.$store.commit("getUserInfo", res);
this.$store.commit("setIsLogin", true);
this.toWechat();
},
});
},
updateOpenid() {
this.$request.post({
url: "bus/info/updateOpenid",
params: {
openid: this.openid,
},
success: (result) => {
this.$message.success("授权成功");
this.type = 2;
},
finally: () => {},
});
},
decideOpenid() {
this.$request.post({
url: "bus/info/decideOpenid",
params: {
openid: this.openid,
},
success: (result) => {
this.type = result;
},
finally: () => {},
});
},
toWechat() {
window.location =
window.location.protocol +
"//" +
window.location.host +
"/newApi/wechat/autho/public/send?type=2";
},
},
};
</script>
<style>
html,
body {
width: 100%;
height: 100%;
min-width: auto;
}
input[type="button"] {
-webkit-appearance: none;
appearance: none;
}
</style>
<style scoped>
#wxLogin {
width: 100%;
height: 100vh;
background-color: #f5f5f5;
}
.form {
padding-top: 100px;
}
.form > img {
width: 40%;
display: block;
margin: 0 auto;
margin-bottom: 20px;
}
.form > input[type="tel"],
.form > input[type="password"] {
width: 80%;
height: 40px;
display: block;
margin: 0 auto;
margin-top: 20px;
border: none;
border-radius: 50px;
background-color: #ffffff;
padding: 0 20px;
box-sizing: border-box;
font-size: 16px;
}
.form .verification {
width: 80%;
height: 40px;
margin: 0 auto;
margin-top: 20px;
display: flex;
justify-content: space-between;
}
.form .verification input {
background: #ffffff;
border: none;
border-radius: 50px;
width: 60%;
height: 100%;
padding: 0 20px;
box-sizing: border-box;
font-size: 16px;
}
.form .verification #imgcode {
width: 38%;
height: 100%;
padding-right: 10px;
cursor: pointer;
margin-left: 10px;
}
.form .verification #imgcode img {
width: 100%;
height: 100%;
display: block;
border-radius: 5px;
}
.form input[type="button"] {
width: 80%;
height: 40px;
display: block;
margin: 0 auto;
margin-top: 20px;
background-color: #ff8800;
border: none;
border-radius: 50px;
color: #ffffff;
font-size: 18px;
letter-spacing: 20px;
padding-left: 20px;
box-sizing: border-box;
}
.form input[type="button"]#authorization {
letter-spacing: 2px;
padding-left: 2px;
box-sizing: border-box;
width: 60%;
}
.form h3 {
font-size: 16px;
color: #4d4d4d;
margin-top: 10px;
text-align: center;
}
.form p {
text-align: center;
color: #808080;
font-size: 14px;
margin-top: 10px;
margin-bottom: 10px;
}
</style>
export default {
// 验证手机号
isPhone(phone){
var reg=/^1[3-9]\d{9}$/;
if(reg.test(phone)){
return true;
}else{
return false;
}
},
/**
* 验证手机号中国(严谨), 根据工信部2019年最新公布的手机号段
* @param { string } value
*/
isPhoneStrict(phone){
var reg=/^(?:(?:\+|00)86)?1(?:(?:3[\d])|(?:4[5-7|9])|(?:5[0-3|5-9])|(?:6[5-7])|(?:7[0-8])|(?:8[\d])|(?:9[1|8|9]))\d{8}$/g;
if(reg.test(phone)){
return true;
}else{
return false;
}
},
// 数字和字母
numandenglish(str){
var reg=/^[A-Za-z0-9]+$/;
if(reg.test(str)){
return str;
}else{
return str.replace(/[^\w\.\/]/ig,'')
}
},
// 验证邮箱
isEmail(email){
var reg=/^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/;
if(reg.test(email)){
return true;
}else{
return false;
}
},
// 验证非中文
noChinese(text){
var reg=/^[\u4e00-\u9fa5]{0,}$/;
if(reg.test(text)){
return '';
}else{
return text;
}
},
//验证正整数(不含零)
isInteger(num){
var reg=/^[1-9]\d*$/;
if(reg.test(num)){
return num;
}else{
return num.replace(/[^\d]/g,'');
}
},
// 验证正数(不含零)
// 可在keyup方法中使用
// num 需要验证的数字
// min 需要限制的最小值
// max 需要限制的最大值
isPositive(num,min,max){
var reg=/^\d+(\.\d+)?$/;
// 小数点出现次数
var n = (num.split('.')).length-1;
// 小数点不止出现一次
if (n > 1) {
return num.substr(0, num.length - 1);
}
// 判断最后一位为小数点
if (num.indexOf(".") != -1 && num.indexOf(".") == (num.length - 1)) {
if (num.length > 1) {
return num;
} else {
return ''
}
}
// 最后一位不是小数点,判断是否为数字,判断小数位数
else if(reg.test(num)){
if(min && Number(num)< min) return min;
if(max && Number(num)> max) return max;
// 小数点后位数大于2,返回两位小数
if (num.indexOf(".") != -1 && num.split('.')[1].length > 2) {
return Number(num).toFixed(2);
}
if(Number(num)>= 0) return num;
return '';
}
// 不纯为数字,清空并返回空
else{
return num.replace(/[^\d(\.\d)?$]/g,'');
}
},
// 验证链接
isLink(link){
if(link.indexOf('https://') == -1 && link.indexOf('http://') == -1){
return false;
}else{
return true;
}
},
/**
* 验证银行卡号(10到30位, 覆盖对公/私账户, 参考微信支付)
* @param { string } value
*/
isBanktNumber(value){
if (/^[1-9]\d{9,29}$/g.test(value)) {
return value;
} else {
return '';
}
},
/**
* 验证座机电话(国内),如: 0341-86091234
* @param { string } value
*/
isLandlineTelephone(value){
if (/\d{3}-\d{8}|\d{4}-\d{7}/g.test(value)) {
return value;
} else {
return '';
}
},
/**
* 身份证号, 支持1/2代(15位/18位数字)
* @param { string } value
*/
isIDCard(value){
if (/(^\d{8}(0\d|10|11|12)([0-2]\d|30|31)\d{3}$)|(^\d{6}(18|19|20)\d{2}(0\d|10|11|12)([0-2]\d|30|31)\d{3}(\d|X|x)$)/g.test(value)) {
return value;
} else {
return '';
}
},
/**
* 验证护照(包含香港、澳门)
* @param { string } value
*/
isPassport(value){
if (/(^[EeKkGgDdSsPpHh]\d{8}$)|(^(([Ee][a-fA-F])|([DdSsPp][Ee])|([Kk][Jj])|([Mm][Aa])|(1[45]))\d{7}$)/g.test(value)) {
return value;
} else {
return '';
}
},
}
搭建效果如下
网站: http://giftweb.pingq.cn/#/index
礼品代发网
搭建起来的效果如上演示,因为篇幅有限源码会在其他教程体现,以上就是如何从0搭建一个礼品网的教程。