This commit is contained in:
mx 2024-10-23 14:41:31 +08:00
parent e4b9074cc9
commit 223981e5e4
15 changed files with 754 additions and 4 deletions

View File

@ -0,0 +1,134 @@
package com.pusong.web.service.impl;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.pusong.common.core.constant.Constants;
import com.pusong.common.core.constant.GlobalConstants;
import com.pusong.common.core.domain.model.EmailLoginBody;
import com.pusong.common.core.domain.model.LoginUser;
import com.pusong.common.core.domain.model.WxLoginBody;
import com.pusong.common.core.enums.LoginType;
import com.pusong.common.core.enums.UserStatus;
import com.pusong.common.core.exception.user.CaptchaExpireException;
import com.pusong.common.core.exception.user.UserException;
import com.pusong.common.core.utils.MessageUtils;
import com.pusong.common.core.utils.StringUtils;
import com.pusong.common.core.utils.ValidatorUtils;
import com.pusong.common.json.utils.JsonUtils;
import com.pusong.common.redis.utils.RedisUtils;
import com.pusong.common.satoken.utils.LoginHelper;
import com.pusong.common.tenant.helper.TenantHelper;
import com.pusong.system.domain.SysUser;
import com.pusong.system.domain.vo.SysClientVo;
import com.pusong.system.domain.vo.SysUserVo;
import com.pusong.system.mapper.SysUserMapper;
import com.pusong.web.domain.vo.LoginVo;
import com.pusong.web.service.IAuthStrategy;
import com.pusong.web.service.SysLoginService;
import com.pusong.web.wxlogin.WxLogin;
import com.pusong.web.wxlogin.WxPhone;
import com.pusong.web.wxlogin.WxUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* 微信认证策略
*
* @author Michelle.Chung
*/
@Slf4j
@Service("wx" + IAuthStrategy.BASE_NAME)
@RequiredArgsConstructor
public class WxAuthStrategy implements IAuthStrategy {
private final SysLoginService loginService;
private final SysUserMapper userMapper;
@Override
public LoginVo login(String body, SysClientVo client) {
WxLoginBody loginBody = JsonUtils.parseObject(body, WxLoginBody.class);
ValidatorUtils.validate(loginBody);
String tenantId = loginBody.getTenantId();
String loginCode = loginBody.getLoginCode();
String phoneCode = loginBody.getPhoneCode();
// 通过邮箱查找用户
SysUserVo user = loadUserByWx(tenantId, loginBody);
// loginService.checkLogin(LoginType.EMAIL, tenantId, user.getUserName(), () -> !validateEmailCode(tenantId, email, emailCode));
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
LoginUser loginUser = loginService.buildLoginUser(user);
loginUser.setClientKey(client.getClientKey());
loginUser.setDeviceType(client.getDeviceType());
SaLoginModel model = new SaLoginModel();
model.setDevice(client.getDeviceType());
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
// 例如: 后台用户30分钟过期 app用户1天过期
model.setTimeout(client.getTimeout());
model.setActiveTimeout(client.getActiveTimeout());
model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
// 生成token
LoginHelper.login(loginUser, model);
LoginVo loginVo = new LoginVo();
loginVo.setAccessToken(StpUtil.getTokenValue());
loginVo.setExpireIn(StpUtil.getTokenTimeout());
loginVo.setClientId(client.getClientId());
return loginVo;
}
/**
* 校验邮箱验证码
*/
private boolean validateEmailCode(String tenantId, String email, String emailCode) {
String code = RedisUtils.getCacheObject(GlobalConstants.CAPTCHA_CODE_KEY + email);
if (StringUtils.isBlank(code)) {
loginService.recordLogininfor(tenantId, email, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
throw new CaptchaExpireException();
}
return code.equals(emailCode);
}
private SysUserVo loadUserByWx(String tenantId, WxLoginBody loginBody) {
return TenantHelper.dynamic(tenantId, () -> {
String loginCode = loginBody.getLoginCode();
String phoneCode = loginBody.getPhoneCode();
String openid = loginBody.getOpenid();
if (openid == null){
openid = WxUtil.login(loginCode).getOpenid();
}
String phoneNumber = null;
if (phoneCode != null){
WxPhone wxPhone = WxUtil.getPhoneNumber(phoneCode, openid);
phoneNumber = wxPhone.getPhoneNumber();
}
SysUserVo user = null;
if (phoneNumber == null){
user = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>()
.eq(phoneNumber == null, SysUser::getOpenid, openid));
if (user == null){
return null;
}
}else{
}
user = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>()
.eq(phoneNumber == null, SysUser::getOpenid, openid)
.eq(phoneNumber != null, SysUser::getPhonenumber, phoneNumber))
;
if (ObjectUtil.isNull(user)) {
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
}
return user;
});
}
}

View File

@ -0,0 +1,287 @@
package com.pusong.web.wxlogin;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.security.cert.X509Certificate;
import java.util.Map;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import com.pusong.common.core.constant.Constants;
import com.pusong.common.core.utils.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 通用http发送方法
*
* @author staffing
*/
public class HttpUtils
{
private static final Logger log = LoggerFactory.getLogger(HttpUtils.class);
public static String getParam(Map<String, String> param){
StringBuilder stringBuilder = new StringBuilder();
for (Map.Entry<String, String> stringStringEntry : param.entrySet()) {
stringBuilder.append(stringStringEntry.getKey()).append("=").append(stringStringEntry.getValue()).append("&");
}
if (stringBuilder.length() > 0){
stringBuilder = stringBuilder.deleteCharAt(stringBuilder.length() - 1);
}
return stringBuilder.toString();
}
/**
* 向指定 URL 发送GET方法的请求
*
* @param url 发送请求的 URL
* @return 所代表远程资源的响应结果
*/
public static String sendGet(String url)
{
return sendGet(url, StringUtils.EMPTY);
}
/**
* 向指定 URL 发送GET方法的请求
*
* @param url 发送请求的 URL
* @param param 请求参数请求参数应该是 name1=value1&name2=value2 的形式
* @return 所代表远程资源的响应结果
*/
public static String sendGet(String url, String param)
{
return sendGet(url, param, Constants.UTF8);
}
/**
* 向指定 URL 发送GET方法的请求
*
* @param url 发送请求的 URL
* @param param 请求参数请求参数应该是 name1=value1&name2=value2 的形式
* @param contentType 编码类型
* @return 所代表远程资源的响应结果
*/
public static String sendGet(String url, String param, String contentType)
{
StringBuilder result = new StringBuilder();
BufferedReader in = null;
try
{
String urlNameString = StringUtils.isNotBlank(param) ? url + "?" + param : url;
log.info("sendGet - {}", urlNameString);
URL realUrl = new URL(urlNameString);
URLConnection connection = realUrl.openConnection();
connection.setRequestProperty("accept", "*/*");
connection.setRequestProperty("connection", "Keep-Alive");
connection.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
connection.connect();
in = new BufferedReader(new InputStreamReader(connection.getInputStream(), contentType));
String line;
while ((line = in.readLine()) != null)
{
result.append(line);
}
log.info("recv - {}", result);
}
catch (ConnectException e)
{
log.error("调用HttpUtils.sendGet ConnectException, url=" + url + ",param=" + param, e);
}
catch (SocketTimeoutException e)
{
log.error("调用HttpUtils.sendGet SocketTimeoutException, url=" + url + ",param=" + param, e);
}
catch (IOException e)
{
log.error("调用HttpUtils.sendGet IOException, url=" + url + ",param=" + param, e);
}
catch (Exception e)
{
log.error("调用HttpsUtil.sendGet Exception, url=" + url + ",param=" + param, e);
}
finally
{
try
{
if (in != null)
{
in.close();
}
}
catch (Exception ex)
{
log.error("调用in.close Exception, url=" + url + ",param=" + param, ex);
}
}
return result.toString();
}
/**
* 向指定 URL 发送POST方法的请求
*
* @param url 发送请求的 URL
* @param param 请求参数请求参数应该是 name1=value1&name2=value2 的形式
* @return 所代表远程资源的响应结果
*/
public static String sendPost(String url, String param)
{
PrintWriter out = null;
BufferedReader in = null;
StringBuilder result = new StringBuilder();
try
{
log.info("sendPost - {}", url);
URL realUrl = new URL(url);
URLConnection conn = realUrl.openConnection();
conn.setRequestProperty("accept", "*/*");
conn.setRequestProperty("connection", "Keep-Alive");
conn.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
conn.setRequestProperty("Accept-Charset", "utf-8");
conn.setRequestProperty("contentType", "utf-8");
conn.setDoOutput(true);
conn.setDoInput(true);
out = new PrintWriter(conn.getOutputStream());
out.print(param);
out.flush();
in = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
String line;
while ((line = in.readLine()) != null)
{
result.append(line);
}
log.info("recv - {}", result);
}
catch (ConnectException e)
{
log.error("调用HttpUtils.sendPost ConnectException, url=" + url + ",param=" + param, e);
}
catch (SocketTimeoutException e)
{
log.error("调用HttpUtils.sendPost SocketTimeoutException, url=" + url + ",param=" + param, e);
}
catch (IOException e)
{
log.error("调用HttpUtils.sendPost IOException, url=" + url + ",param=" + param, e);
}
catch (Exception e)
{
log.error("调用HttpsUtil.sendPost Exception, url=" + url + ",param=" + param, e);
}
finally
{
try
{
if (out != null)
{
out.close();
}
if (in != null)
{
in.close();
}
}
catch (IOException ex)
{
log.error("调用in.close Exception, url=" + url + ",param=" + param, ex);
}
}
return result.toString();
}
public static String sendSSLPost(String url, String param)
{
StringBuilder result = new StringBuilder();
String urlNameString = url + "?" + param;
try
{
log.info("sendSSLPost - {}", urlNameString);
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, new TrustManager[] { new TrustAnyTrustManager() }, new java.security.SecureRandom());
URL console = new URL(urlNameString);
HttpsURLConnection conn = (HttpsURLConnection) console.openConnection();
conn.setRequestProperty("accept", "*/*");
conn.setRequestProperty("connection", "Keep-Alive");
conn.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
conn.setRequestProperty("Accept-Charset", "utf-8");
conn.setRequestProperty("contentType", "utf-8");
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setSSLSocketFactory(sc.getSocketFactory());
conn.setHostnameVerifier(new TrustAnyHostnameVerifier());
conn.connect();
InputStream is = conn.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String ret = "";
while ((ret = br.readLine()) != null)
{
if (ret != null && !"".equals(ret.trim()))
{
result.append(new String(ret.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8));
}
}
log.info("recv - {}", result);
conn.disconnect();
br.close();
}
catch (ConnectException e)
{
log.error("调用HttpUtils.sendSSLPost ConnectException, url=" + url + ",param=" + param, e);
}
catch (SocketTimeoutException e)
{
log.error("调用HttpUtils.sendSSLPost SocketTimeoutException, url=" + url + ",param=" + param, e);
}
catch (IOException e)
{
log.error("调用HttpUtils.sendSSLPost IOException, url=" + url + ",param=" + param, e);
}
catch (Exception e)
{
log.error("调用HttpsUtil.sendSSLPost Exception, url=" + url + ",param=" + param, e);
}
return result.toString();
}
private static class TrustAnyTrustManager implements X509TrustManager
{
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType)
{
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType)
{
}
@Override
public X509Certificate[] getAcceptedIssuers()
{
return new X509Certificate[] {};
}
}
private static class TrustAnyHostnameVerifier implements HostnameVerifier
{
@Override
public boolean verify(String hostname, SSLSession session)
{
return true;
}
}
}

View File

@ -0,0 +1,49 @@
package com.pusong.web.wxlogin;
public class WxLogin {
private String session_key;
private String unionid;//用户在开放平台的唯一标识符若当前小程序已绑定到微信开放平台账号下会返回详见 UnionID 机制说明
private String errmsg;
private String openid;
private int errcode;
public String getSession_key() {
return session_key;
}
public void setSession_key(String session_key) {
this.session_key = session_key;
}
public String getUnionid() {
return unionid;
}
public void setUnionid(String unionid) {
this.unionid = unionid;
}
public String getErrmsg() {
return errmsg;
}
public void setErrmsg(String errmsg) {
this.errmsg = errmsg;
}
public String getOpenid() {
return openid;
}
public void setOpenid(String openid) {
this.openid = openid;
}
public int getErrcode() {
return errcode;
}
public void setErrcode(int errcode) {
this.errcode = errcode;
}
}

View File

@ -0,0 +1,118 @@
package com.pusong.web.wxlogin;
public class WxPhone {
private int errcode;//有效期
private String errmsg;//有效期
private PhoneInfo phone_info;//有效期
static class PhoneInfo{
String phoneNumber;
String purePhoneNumber;
int countryCode;
Watermark watermark;
public String getPhoneNumber() {
return phoneNumber;
}
public void setPhoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
}
public String getPurePhoneNumber() {
return purePhoneNumber;
}
public void setPurePhoneNumber(String purePhoneNumber) {
this.purePhoneNumber = purePhoneNumber;
}
public int getCountryCode() {
return countryCode;
}
public void setCountryCode(int countryCode) {
this.countryCode = countryCode;
}
public Watermark getWatermark() {
return watermark;
}
public void setWatermark(Watermark watermark) {
this.watermark = watermark;
}
}
static class Watermark{
int timestamp;
String appid;
}
private String access_token;
private int expires_in;//有效期
private long expiresTime;//到期时间
public int getErrcode() {
return errcode;
}
public void setErrcode(int errcode) {
this.errcode = errcode;
}
public String getErrmsg() {
return errmsg;
}
public void setErrmsg(String errmsg) {
this.errmsg = errmsg;
}
public PhoneInfo getPhone_info() {
return phone_info;
}
public void setPhone_info(PhoneInfo phone_info) {
this.phone_info = phone_info;
}
public String getAccess_token() {
return access_token;
}
public void setAccess_token(String access_token) {
this.access_token = access_token;
}
public int getExpires_in() {
return expires_in;
}
public void setExpires_in(int expires_in) {
this.expires_in = expires_in;
}
public long getExpiresTime() {
return expiresTime;
}
public void setExpiresTime(long expiresTime) {
this.expiresTime = expiresTime;
}
public String getPhoneNumber() {
return this.phone_info.phoneNumber;
}
public String getPurePhoneNumber() {
return this.phone_info.purePhoneNumber;
}
}

View File

@ -0,0 +1,32 @@
package com.pusong.web.wxlogin;
public class WxToken {
private String access_token;
private int expires_in;//有效期
private long expiresTime;//到期时间
public String getAccess_token() {
return access_token;
}
public void setAccess_token(String access_token) {
this.access_token = access_token;
}
public int getExpires_in() {
return expires_in;
}
public void setExpires_in(int expires_in) {
this.expires_in = expires_in;
}
public long getExpiresTime() {
return expiresTime;
}
public void setExpiresTime(long expiresTime) {
this.expiresTime = expiresTime;
}
}

View File

@ -0,0 +1,62 @@
package com.pusong.web.wxlogin;
import com.alibaba.fastjson.JSONObject;
import java.util.HashMap;
import java.util.Map;
public class WxUtil {
public final static String appId = "wx7bc7df6eb945a84f";//ps
public final static String secret = "60dc8f7e8c850dfbe0170acefa48070a";//ps
// public final static String appId = "wx26e952dad7a8aae5";
// public final static String secret = "60a85e94d921a385ce52faaaed4a103b";
private static WxToken wxToken = null;
public static synchronized WxToken getAccessToken(){
if (wxToken == null || System.currentTimeMillis() > wxToken.getExpiresTime() - 60000){
Map<String, String > getParam = new HashMap<>();
getParam.put("grant_type", "client_credential");
getParam.put("appid", appId);
getParam.put("secret", secret);
String resJson = HttpUtils.sendGet("https://api.weixin.qq.com/cgi-bin/token", HttpUtils.getParam(getParam));
wxToken = JSONObject.parseObject(resJson, WxToken.class);
wxToken.setExpiresTime(wxToken.getExpires_in() * 1000 + System.currentTimeMillis());
}
return wxToken;
}
/**
* 获取手机号码
* @param code 前端api获取后 传的
* @param openId 微信用户唯一表示
* @return
*/
public static WxPhone getPhoneNumber(String code, String openId){
Map<String, String > getParam = new HashMap<>();
getParam.put("code", code);
getParam.put("openid", openId);
String url = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=" + getAccessToken().getAccess_token();
String resJson = HttpUtils.sendPost(url, JSONObject.toJSONString(getParam));
WxPhone wxPhone = JSONObject.parseObject(resJson, WxPhone.class);
return wxPhone;
}
/**
* wx 登录 获取openId
* @return
*/
public static WxLogin login(String code){
Map<String, String > getParam = new HashMap<>();
getParam.put("grant_type", "authorization_code");
getParam.put("appid", appId);
getParam.put("secret", secret);
getParam.put("js_code", code);
String url = "https://api.weixin.qq.com/sns/jscode2session";
String resJson = HttpUtils.sendGet(url, HttpUtils.getParam(getParam));
WxLogin WxLogin = JSONObject.parseObject(resJson, WxLogin.class);
return WxLogin;
}
}

View File

@ -0,0 +1,33 @@
package com.pusong.common.core.domain.model;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* wx登录对象
*
* @author Lion Li
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class WxLoginBody extends LoginBody {
/**
* loginCode
*/
private String loginCode;
/**
* openid
*/
private String openid;
/**
* phoneCode
*/
private String phoneCode;
}

View File

@ -59,7 +59,7 @@ public class PsCustomInfo extends TenantEntity {
private String customLevel;
/**
* 客户状态
* 客户状态 CustomerStatusEnum
*/
private String customStatus;

View File

@ -76,7 +76,7 @@ public class PsCustomInfoVo implements Serializable {
private String customLevel;
/**
* 客户状态
* 客户状态 CustomerStatusEnum
*/
@ExcelProperty(value = "客户状态", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "customer_status")

View File

@ -147,6 +147,8 @@ public class PsCompanyInfoServiceImpl implements IPsCompanyInfoService {
sql.append("and business_type = '2' and is_proxy = '2'");
}
lqw.exists(bo.getType() != 1, sql.toString());
lqw.orderByDesc("task.finish_date");
// lqw.groupBy("com.id");
Page<PsCompanyInfoVo> result = baseMapper.selectPageList2(pageQuery.build(), lqw);
if (!result.getRecords().isEmpty()){
//填充代账服务项目

View File

@ -1,8 +1,10 @@
package com.pusong.business.service.impl;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@ -278,7 +280,29 @@ public class PsCustomInfoServiceImpl implements IPsCustomInfoService {
public Boolean updateByBo(PsCustomInfoBo bo) {
PsCustomInfo update = MapstructUtils.convert(bo, PsCustomInfo.class);
validEntityBeforeSave(update);
baseMapper.updateById(update);
UpdateWrapper<PsCustomInfo> wrapper = new UpdateWrapper<>();
wrapper.eq("id", update.getId())
.set("custom_name", update.getCustomName())
.set("custom_mobile", update.getCustomMobile())
.set("custom_source", update.getCustomSource())
.set("custom_introducer", update.getCustomIntroducer())
// .set("custom_adress_detail", update.getCustomAdressDetail())
// .set("custom_manager", update.getCustomManager())
.set("custom_level", update.getCustomLevel())
.set("create_time", update.getCreateTime())
// .set("custom_status", update.getCustomStatus())
// .set("black", update.getBlack())
// .set("black_desc", update.getBlackDesc())
// .set("color", update.getColor())
// .set("history_custom_manager", update.getHistoryCustomManager())
// .set("del_flag", delFlag)
// .set("accept_date", update.getAcceptDate())
.set("update_time", new Date())
;
baseMapper.update(wrapper);
//修改公司信息
if(CollectionUtils.isNotEmpty(bo.getCompanyInfoBos())){
bo.getCompanyInfoBos().forEach(item->{item.setCustomId(bo.getId());item.setCompanyType("1");});

View File

@ -88,6 +88,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
left join ps_custom_info cusi on cus.custom_introducer = cusi.id
left join ps_company_follow cf on com.id = cf.company_id
left join sys_user usr on cf.user_id = usr.user_id
inner join ps_task_main task on task.service_company_id = com.id and task.del_flag = 0
left join (
SELECT business_id, max(apply_date) max_apply_date
FROM ps_approver_record

View File

@ -19,7 +19,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
start_date
,EXISTS(SELECT 1 FROM ps_contract_info coninfo WHERE <include refid="queryContract"/>) have_contract
FROM ps_custom_info info
left join sys_user usr on info.custom_manager = usr.user_id
left join sys_user usr on info.custom_manager = usr.user_id or info.history_custom_manager = usr.user_id
left join ps_custom_info psinfo on info.custom_introducer = psinfo.id
</sql>

View File

@ -103,6 +103,11 @@ public class SysUser extends TenantEntity {
*/
private String remark;
/**
* 微信 openid
*/
private String openid;
public SysUser(Long userId) {
this.userId = userId;

View File

@ -1,3 +1,6 @@
alter table ps_company_info add `contact_person_name` varchar(15) comment '联系人姓名';
alter table ps_company_info add `contact_person_phone` varchar(15) comment '联系人电话';
alter table ps_company_info add `contact_person_idcard` varchar(20) comment '联系人身份证号';
alter table sys_user add `openid` varchar(30) comment '微信openid';