소스 검색

协议完成;登录完成一半

Huanyi 3 주 전
부모
커밋
28ead360ae
28개의 변경된 파일1234개의 추가작업 그리고 20개의 파일을 삭제
  1. 83 0
      ruoyi-admin/src/main/java/org/dromara/web/controller/AuthController.java
  2. 47 0
      ruoyi-admin/src/main/java/org/dromara/web/service/SysRegisterService.java
  3. 117 0
      ruoyi-admin/src/main/java/org/dromara/web/service/impl/WechatAppletAuthStrategy.java
  4. 11 0
      ruoyi-admin/src/main/resources/application-dev.yml
  5. 11 0
      ruoyi-admin/src/main/resources/application-prod.yml
  6. 12 12
      ruoyi-admin/src/main/resources/application.yml
  7. 5 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheNames.java
  8. 18 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/WechatAppletLoginBody.java
  9. 27 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/WechatAppletRegisterBody.java
  10. 20 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/WechatPhoneBody.java
  11. 21 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/wechat/config/WechatConfig.java
  12. 8 8
      ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/filter/CryptoFilter.java
  13. 56 0
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysAgreementController.java
  14. 81 0
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysCustomerController.java
  15. 40 0
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/SysAgreement.java
  16. 68 0
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/SysCustomer.java
  17. 42 0
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/bo/SysAgreementBo.java
  18. 63 0
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/bo/SysCustomerBo.java
  19. 43 0
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysAgreementVo.java
  20. 74 0
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysCustomerVo.java
  21. 13 0
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysAgreementMapper.java
  22. 14 0
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysCustomerMapper.java
  23. 23 0
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysAgreementService.java
  24. 46 0
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysCustomerService.java
  25. 51 0
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysAgreementServiceImpl.java
  26. 90 0
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysCustomerServiceImpl.java
  27. 142 0
      script/sql/sqlserver/v1/create.sql
  28. 8 0
      script/sql/sqlserver/v1/update.sql

+ 83 - 0
ruoyi-admin/src/main/java/org/dromara/web/controller/AuthController.java

@@ -5,7 +5,10 @@ import cn.dev33.satoken.exception.NotLoginException;
 import cn.dev33.satoken.stp.StpUtil;
 import cn.hutool.core.codec.Base64;
 import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.Dict;
 import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpResponse;
 import jakarta.servlet.http.HttpServletRequest;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -18,6 +21,9 @@ import org.dromara.common.core.domain.R;
 import org.dromara.common.core.domain.model.LoginBody;
 import org.dromara.common.core.domain.model.RegisterBody;
 import org.dromara.common.core.domain.model.SocialLoginBody;
+import org.dromara.common.core.domain.model.WechatAppletRegisterBody;
+import org.dromara.common.core.domain.model.WechatPhoneBody;
+import org.dromara.common.core.exception.ServiceException;
 import org.dromara.common.core.utils.*;
 import org.dromara.common.encrypt.annotation.ApiEncrypt;
 import org.dromara.common.json.utils.JsonUtils;
@@ -37,6 +43,7 @@ import org.dromara.system.service.ISysClientService;
 import org.dromara.system.service.ISysConfigService;
 import org.dromara.system.service.ISysSocialService;
 import org.dromara.system.service.ISysTenantService;
+import org.dromara.common.wechat.config.WechatConfig;
 import org.dromara.web.domain.vo.LoginTenantVo;
 import org.dromara.web.domain.vo.LoginVo;
 import org.dromara.web.domain.vo.TenantListVo;
@@ -74,6 +81,7 @@ public class AuthController {
     private final ISysTenantService tenantService;
     private final ISysSocialService socialUserService;
     private final ISysClientService clientService;
+    private final WechatConfig wechatConfig;
     private final ScheduledExecutorService scheduledExecutorService;
 
 
@@ -196,6 +204,81 @@ public class AuthController {
         return R.ok();
     }
 
+    @PostMapping("/wechat/phone")
+    public R<String> getWechatPhone(@Validated @RequestBody WechatPhoneBody body) {
+        String phoneCode = body.getPhoneCode();
+        String openId = body.getOpenId();
+
+        if (StringUtils.isEmpty(phoneCode)) {
+            return R.fail("手机号code不能为空");
+        }
+        if (StringUtils.isEmpty(openId)) {
+            return R.fail("openId不能为空");
+        }
+
+        String appId = wechatConfig.getAppId();
+        String appSecret = wechatConfig.getAppSecret();
+
+        String token = getTokenByAppIdAndSecret(appId, appSecret);
+        if (StringUtils.isEmpty(token)) {
+            return R.fail("微信平台获取凭证失败");
+        }
+
+        String phoneNumber = getPhoneNumberByCodeAndOpenId(phoneCode, openId, token);
+        return R.ok("获取成功", phoneNumber);
+    }
+
+    @PostMapping("/wechat/register")
+    public R<Void> wechatRegister(@Validated @RequestBody WechatAppletRegisterBody body) {
+        String openId = body.getOpenId();
+        String unionId = body.getUnionId();
+        String phone = body.getPhone();
+        String nickname = body.getNickname();
+        String avatar = body.getAvatar();
+
+        if (StringUtils.isEmpty(openId) || StringUtils.isEmpty(unionId) || StringUtils.isEmpty(phone)) {
+            return R.fail("必要参数不能为空");
+        }
+
+        registerService.wechatRegister(openId, unionId, phone, nickname, avatar);
+        return R.ok();
+    }
+
+    private String getTokenByAppIdAndSecret(String appId, String appSecret) {
+        String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + appId + "&secret=" + appSecret;
+        HttpResponse response = HttpRequest.get(url).execute();
+        String body = response.body();
+        Dict map = JsonUtils.parseMap(body);
+        log.info("返回 : {}", body);
+        if (map == null || map.isEmpty()) {
+            return null;
+        }
+        return map.getStr("access_token");
+    }
+
+    private String getPhoneNumberByCodeAndOpenId(String code, String openId, String token) {
+        Dict dto = Dict.create().set("code", code);
+        String request = JsonUtils.toJsonString(dto);
+        log.info("dto : {}", dto);
+        String response = HttpRequest.post("https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=" + token)
+            .body(request)
+            .execute()
+            .body();
+
+        Dict responseDict = JsonUtils.parseMap(response);
+        if (responseDict == null) {
+            throw new ServiceException("解析失败");
+        }
+
+        if (responseDict.containsKey("errcode") && responseDict.getInt("errcode") != 0) {
+            throw new ServiceException("获取手机号失败: " + responseDict.getStr("errmsg"));
+        }
+
+        Object phoneInfo = responseDict.get("phone_info");
+        Dict phoneInformation = JsonUtils.parseMap(JsonUtils.toJsonString(phoneInfo));
+        return phoneInformation.getStr("purePhoneNumber");
+    }
+
     /**
      * 登录页面租户下拉框
      *

+ 47 - 0
ruoyi-admin/src/main/java/org/dromara/web/service/SysRegisterService.java

@@ -1,5 +1,7 @@
 package org.dromara.web.service;
 
+import cn.hutool.core.codec.Base64;
+import cn.hutool.core.io.FileUtil;
 import cn.hutool.crypto.digest.BCrypt;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import lombok.RequiredArgsConstructor;
@@ -18,9 +20,13 @@ import org.dromara.common.log.event.LogininforEvent;
 import org.dromara.common.redis.utils.RedisUtils;
 import org.dromara.common.tenant.helper.TenantHelper;
 import org.dromara.common.web.config.properties.CaptchaProperties;
+import org.dromara.system.domain.SysCustomer;
 import org.dromara.system.domain.SysUser;
 import org.dromara.system.domain.bo.SysUserBo;
+import org.dromara.system.domain.vo.SysOssVo;
+import org.dromara.system.mapper.SysCustomerMapper;
 import org.dromara.system.mapper.SysUserMapper;
+import org.dromara.system.service.ISysOssService;
 import org.dromara.system.service.ISysUserService;
 import org.springframework.stereotype.Service;
 
@@ -35,6 +41,8 @@ public class SysRegisterService {
 
     private final ISysUserService userService;
     private final SysUserMapper userMapper;
+    private final SysCustomerMapper customerMapper;
+    private final ISysOssService ossService;
     private final CaptchaProperties captchaProperties;
 
     /**
@@ -112,4 +120,43 @@ public class SysRegisterService {
         SpringUtils.context().publishEvent(logininforEvent);
     }
 
+    public void wechatRegister(String openId, String unionId, String phone, String nickname, String avatar) {
+        SysCustomer customer = new SysCustomer();
+        customer.setWechatOpenid(openId);
+        customer.setWechatUnionid(unionId);
+        customer.setPhone(phone);
+        customer.setUserName(StringUtils.isNotEmpty(nickname) ? nickname : "用户" + phone.substring(phone.length() - 4));
+        
+        if (StringUtils.isNotEmpty(avatar)) {
+            try {
+                String base64Data = avatar;
+                if (base64Data.startsWith("data:image/")) {
+                    String[] parts = base64Data.split(",");
+                    if (parts.length == 2) {
+                        String imageData = parts[1];
+                        String contentType = parts[0].split(";")[0].split(":")[1];
+                        String suffix = contentType.split("/")[1];
+                        
+                        byte[] imageBytes = Base64.decode(imageData);
+                        String fileName = "avatar_" + System.currentTimeMillis() + "." + suffix;
+                        java.io.File tempFile = java.io.File.createTempFile("upload_", fileName);
+                        FileUtil.writeBytes(imageBytes, tempFile);
+                        
+                        SysOssVo ossVo = ossService.upload(tempFile);
+                        customer.setAvatar(ossVo.getOssId());
+                        
+                        tempFile.delete();
+                    }
+                }
+            } catch (Exception e) {
+                throw new UserException("头像上传失败: " + e.getMessage());
+            }
+        }
+        
+        int result = customerMapper.insert(customer);
+        if (result <= 0) {
+            throw new UserException("用户注册失败");
+        }
+    }
+
 }

+ 117 - 0
ruoyi-admin/src/main/java/org/dromara/web/service/impl/WechatAppletAuthStrategy.java

@@ -0,0 +1,117 @@
+package org.dromara.web.service.impl;
+
+import cn.dev33.satoken.stp.StpUtil;
+import cn.dev33.satoken.stp.parameter.SaLoginParameter;
+import cn.hutool.core.lang.Dict;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.domain.model.WechatAppletLoginBody;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.core.utils.ValidatorUtils;
+import org.dromara.common.json.utils.JsonUtils;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.common.wechat.config.WechatConfig;
+import org.dromara.system.domain.vo.SysClientVo;
+import org.dromara.system.domain.vo.SysCustomerVo;
+import org.dromara.system.service.ISysCustomerService;
+import org.dromara.web.domain.vo.LoginVo;
+import org.dromara.web.service.IAuthStrategy;
+import org.springframework.stereotype.Service;
+
+import java.util.Objects;
+
+@Slf4j
+@Service("wechatApplet" + IAuthStrategy.BASE_NAME)
+@RequiredArgsConstructor
+public class WechatAppletAuthStrategy implements IAuthStrategy {
+
+    private final WechatConfig wechatConfig;
+    private final ISysCustomerService customerService;
+
+    @Override
+    public LoginVo login(String body, SysClientVo client) {
+        WechatAppletLoginBody loginBody = JsonUtils.parseObject(body, WechatAppletLoginBody.class);
+        ValidatorUtils.validate(loginBody);
+
+        String loginCode = loginBody.getLoginCode();
+        if (StringUtils.isEmpty(loginCode)) {
+            throw new ServiceException("登录code不能为空");
+        }
+
+        String appId = wechatConfig.getAppId();
+        String appSecret = wechatConfig.getAppSecret();
+
+        String loginInformation = getLoginInfoByCode(loginCode, appId, appSecret);
+        Dict response;
+        try {
+            response = JsonUtils.parseMap(loginInformation);
+        } catch (Exception e) {
+            log.error("微信登录返回值解析失败 : {}", e.getMessage());
+            throw new ServiceException("微信平台对接失败");
+        }
+
+        if (Objects.isNull(response)) {
+            throw new ServiceException("微信平台对接失败");
+        }
+
+        if (response.containsKey("errcode") && response.getInt("errcode") != 0) {
+            log.error("微信登陆失败 : {}", response.getStr("errmsg"));
+            throw new ServiceException("微信登陆失败 : 获取 open ID 失败");
+        }
+
+        if (!response.containsKey("openid")) {
+            throw new ServiceException("open ID 获取失败!");
+        }
+        String openId = response.getStr("openid");
+
+        String unionId = response.containsKey("unionid") ? response.getStr("unionid") : "";
+
+        SysCustomerVo customer = customerService.queryByWechatOpenid(openId);
+
+        if (Objects.isNull(customer)) {
+            LoginVo loginVo = new LoginVo();
+            loginVo.setOpenid(openId);
+            return loginVo;
+        }
+
+        LoginVo loginVo = new LoginVo();
+        loginVo.setAccessToken(generateToken(customer.getId(), client));
+        loginVo.setExpireIn(StpUtil.getTokenTimeout());
+        loginVo.setClientId(client.getClientId());
+        loginVo.setOpenid(openId);
+        return loginVo;
+    }
+
+    private String getLoginInfoByCode(String code, String appId, String appSecret) {
+        String url = wechatConfig.getLoginUrl() + "?appid=" + appId + "&secret=" + appSecret + "&js_code=" + code + "&grant_type=authorization_code";
+        String result;
+
+        try {
+            HttpResponse response = HttpRequest.get(url).execute();
+            result = response.body();
+            log.info("微信一键授权登录返回信息 : {}", result);
+        } catch (Exception e) {
+            log.error("获取OpenId 发送GET 请求错误 ====>  {}", e.getMessage());
+            throw new ServiceException("微信登录失败!");
+        }
+
+        if (StringUtils.isEmpty(result)) {
+            throw new ServiceException("微信登录失败!");
+        }
+
+        return result;
+    }
+
+    private String generateToken(Long customerId, SysClientVo client) {
+        String loginId = "customer:" + customerId;
+        StpUtil.login(loginId, new SaLoginParameter()
+            .setDeviceType(client.getDeviceType())
+            .setTimeout(client.getTimeout())
+            .setActiveTimeout(client.getActiveTimeout())
+            .setExtra(LoginHelper.CLIENT_KEY, client.getClientId()));
+        return StpUtil.getTokenValue();
+    }
+}

+ 11 - 0
ruoyi-admin/src/main/resources/application-dev.yml

@@ -1,3 +1,14 @@
+--- # 验证码配置
+captcha:
+  # 是否启用验证码校验
+  enable: false
+  # 验证码类型 math 数组计算 char 字符验证
+  type: math
+  # 数字验证码位数
+  numberLength: 1
+  # 字符验证码长度
+  charLength: 4
+
 --- # 监控中心配置
 spring.boot.admin.client:
   # 增加客户端开关

+ 11 - 0
ruoyi-admin/src/main/resources/application-prod.yml

@@ -1,6 +1,17 @@
 --- # 临时文件存储位置 避免临时文件被系统清理报错
 spring.servlet.multipart.location: /ruoyi/server/temp
 
+--- # 验证码配置
+captcha:
+  # 是否启用验证码校验
+  enable: true
+  # 验证码类型 math 数组计算 char 字符验证
+  type: math
+  # 数字验证码位数
+  numberLength: 1
+  # 字符验证码长度
+  charLength: 4
+
 --- # 监控中心配置
 spring.boot.admin.client:
   # 增加客户端开关

+ 12 - 12
ruoyi-admin/src/main/resources/application.yml

@@ -20,16 +20,6 @@ server:
       # 阻塞任务线程池, 当执行类似servlet请求阻塞操作, undertow会从这个线程池中取得线程,它的值设置取决于系统的负载
       worker: 256
 
-captcha:
-  # 是否启用验证码校验
-  enable: true
-  # 验证码类型 math 数组计算 char 字符验证
-  type: math
-  # 数字验证码位数
-  numberLength: 1
-  # 字符验证码长度
-  charLength: 4
-
 # 日志配置
 logging:
   level:
@@ -120,7 +110,7 @@ security:
 # 多租户配置
 tenant:
   # 是否开启
-  enable: true
+  enable: false
   # 排除表
   excludes:
     - sys_menu
@@ -133,6 +123,7 @@ tenant:
     - sys_client
     - sys_oss_config
     - flow_spel
+    - sys_customer
 
 # MyBatisPlus配置
 # https://baomidou.com/config/
@@ -239,7 +230,7 @@ management:
 
 --- # 默认/推荐使用sse推送
 sse:
-  enabled: true
+  enabled: false
   path: /resource/sse
 
 --- # websocket
@@ -263,3 +254,12 @@ warm-flow:
   node-tooltip: true
   # 默认Authorization,如果有多个token,用逗号分隔
   token-name: ${sa-token.token-name},clientid
+
+# 微信小程序配置
+wechat:
+  mini-program:
+    app-id: wxa204c3f52e3827bd
+    app-secret: ecb78f23deee53944ae3f915b9b8a9fb
+    login-url: https://api.weixin.qq.com/sns/jscode2session
+    token-url: https://api.weixin.qq.com/cgi-bin/token
+    phone-url: https://api.weixin.qq.com/wxa/business/getuserphonenumber

+ 5 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheNames.java

@@ -81,6 +81,11 @@ public interface CacheNames {
      */
     String SYS_OSS_CONFIG = GlobalConstants.GLOBAL_REDIS_KEY + "sys_oss_config";
 
+    /**
+     * 协议信息
+     */
+    String SYS_AGREEMENT = GlobalConstants.GLOBAL_REDIS_KEY + "sys_agreement";
+
     /**
      * 在线用户
      */

+ 18 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/WechatAppletLoginBody.java

@@ -0,0 +1,18 @@
+package org.dromara.common.core.domain.model;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serial;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class WechatAppletLoginBody extends LoginBody {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @NotBlank(message = "登录code不能为空")
+    private String loginCode;
+}

+ 27 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/WechatAppletRegisterBody.java

@@ -0,0 +1,27 @@
+package org.dromara.common.core.domain.model;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+@Data
+public class WechatAppletRegisterBody implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @NotBlank(message = "openId不能为空")
+    private String openId;
+
+    @NotBlank(message = "unionId不能为空")
+    private String unionId;
+
+    @NotBlank(message = "手机号不能为空")
+    private String phone;
+
+    private String nickname;
+
+    private String avatar;
+}

+ 20 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/WechatPhoneBody.java

@@ -0,0 +1,20 @@
+package org.dromara.common.core.domain.model;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+@Data
+public class WechatPhoneBody implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @NotBlank(message = "手机号code不能为空")
+    private String phoneCode;
+
+    @NotBlank(message = "openId不能为空")
+    private String openId;
+}

+ 21 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/wechat/config/WechatConfig.java

@@ -0,0 +1,21 @@
+package org.dromara.common.wechat.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@Data
+@Component
+@ConfigurationProperties(prefix = "wechat.mini-program")
+public class WechatConfig {
+
+    private String appId;
+
+    private String appSecret;
+
+    private String loginUrl;
+
+    private String tokenUrl;
+
+    private String phoneUrl;
+}

+ 8 - 8
ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/filter/CryptoFilter.java

@@ -50,14 +50,14 @@ public class CryptoFilter implements Filter {
                 // 请求解密
                 requestWrapper = new DecryptRequestBodyWrapper(servletRequest, properties.getPrivateKey(), properties.getHeaderFlag());
             } else {
-                // 是否有注解,有就报错,没有放行
-                if (ObjectUtil.isNotNull(apiEncrypt)) {
-                    HandlerExceptionResolver exceptionResolver = SpringUtils.getBean("handlerExceptionResolver", HandlerExceptionResolver.class);
-                    exceptionResolver.resolveException(
-                        servletRequest, servletResponse, null,
-                        new ServiceException("没有访问权限,请联系管理员授权", HttpStatus.FORBIDDEN));
-                    return;
-                }
+//                // 是否有注解,有就报错,没有放行
+//                if (ObjectUtil.isNotNull(apiEncrypt)) {
+//                    HandlerExceptionResolver exceptionResolver = SpringUtils.getBean("handlerExceptionResolver", HandlerExceptionResolver.class);
+//                    exceptionResolver.resolveException(
+//                        servletRequest, servletResponse, null,
+//                        new ServiceException("没有访问权限,请联系管理员授权", HttpStatus.FORBIDDEN));
+//                    return;
+//                }
             }
         }
 

+ 56 - 0
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysAgreementController.java

@@ -0,0 +1,56 @@
+package org.dromara.system.controller.system;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import cn.dev33.satoken.annotation.SaIgnore;
+import jakarta.validation.constraints.NotNull;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.validate.EditGroup;
+import org.dromara.common.idempotent.annotation.RepeatSubmit;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.log.enums.BusinessType;
+import org.dromara.common.web.core.BaseController;
+import org.dromara.system.domain.bo.SysAgreementBo;
+import org.dromara.system.domain.vo.SysAgreementVo;
+import org.dromara.system.service.ISysAgreementService;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 协议信息管理
+ *
+ * @author Lion Li
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/system/agreement")
+public class SysAgreementController extends BaseController {
+
+    private final ISysAgreementService sysAgreementService;
+
+    /**
+     * 根据ID获取协议详情
+     *
+     * @param id 协议ID(1=用户协议,2=隐私政策)
+     */
+    @SaIgnore
+    @GetMapping("/{id}")
+    public R<SysAgreementVo> getInfo(@NotNull(message = "协议ID不能为空")
+                                     @PathVariable Long id) {
+        return R.ok(sysAgreementService.queryById(id));
+    }
+
+    /**
+     * 修改协议内容(前端传入 base64 编码的 HTML 富文本)
+     */
+    @SaCheckPermission("system:agreement:edit")
+    @Log(title = "协议管理", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
+    @PutMapping()
+    public R<Void> edit(@Validated(EditGroup.class) @RequestBody SysAgreementBo bo) {
+        sysAgreementService.updateByBo(bo);
+        return R.ok();
+    }
+
+}

+ 81 - 0
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysCustomerController.java

@@ -0,0 +1,81 @@
+package org.dromara.system.controller.system;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.validate.EditGroup;
+import org.dromara.common.idempotent.annotation.RepeatSubmit;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.log.enums.BusinessType;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.web.core.BaseController;
+import org.dromara.system.domain.bo.SysCustomerBo;
+import org.dromara.system.domain.vo.SysCustomerVo;
+import org.dromara.system.service.ISysCustomerService;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+
+/**
+ * 客户管理
+ *
+ * @author Lion Li
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/system/customer")
+public class SysCustomerController extends BaseController {
+
+    private final ISysCustomerService sysCustomerService;
+
+    /**
+     * 查询客户列表
+     */
+    @SaCheckPermission("system:customer:list")
+    @GetMapping("/list")
+    public TableDataInfo<SysCustomerVo> list(SysCustomerBo bo, PageQuery pageQuery) {
+        return sysCustomerService.queryPageList(bo, pageQuery);
+    }
+
+    /**
+     * 获取客户详细信息
+     *
+     * @param id 主键
+     */
+    @SaCheckPermission("system:customer:query")
+    @GetMapping("/{id}")
+    public R<SysCustomerVo> getInfo(@NotNull(message = "主键不能为空")
+                                    @PathVariable Long id) {
+        return R.ok(sysCustomerService.queryById(id));
+    }
+
+    /**
+     * 修改客户
+     */
+    @SaCheckPermission("system:customer:edit")
+    @Log(title = "客户管理", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
+    @PutMapping()
+    public R<Void> edit(@Validated(EditGroup.class) @RequestBody SysCustomerBo bo) {
+        return toAjax(sysCustomerService.updateByBo(bo));
+    }
+
+    /**
+     * 删除客户
+     *
+     * @param ids 主键串
+     */
+    @SaCheckPermission("system:customer:remove")
+    @Log(title = "客户管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public R<Void> remove(@NotEmpty(message = "主键不能为空")
+                          @PathVariable Long[] ids) {
+        return toAjax(sysCustomerService.deleteWithValidByIds(List.of(ids), true));
+    }
+}

+ 40 - 0
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/SysAgreement.java

@@ -0,0 +1,40 @@
+package org.dromara.system.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+
+import java.io.Serial;
+
+/**
+ * 协议信息表 sys_agreement
+ *
+ * @author Lion Li
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("sys_agreement")
+public class SysAgreement extends BaseEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 协议ID
+     */
+    @TableId(value = "id")
+    private Long id;
+
+    /**
+     * 协议标题
+     */
+    private String title;
+
+    /**
+     * 协议内容(富文本HTML)
+     */
+    private String content;
+
+}

+ 68 - 0
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/SysCustomer.java

@@ -0,0 +1,68 @@
+package org.dromara.system.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.tenant.core.TenantEntity;
+
+import java.io.Serial;
+
+/**
+ * 客户表 sys_customer
+ *
+ * @author Lion Li
+ */
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("sys_customer")
+public class SysCustomer extends TenantEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 客户ID
+     */
+    @TableId(value = "id")
+    private Long id;
+
+    /**
+     * 用户名
+     */
+    private String userName;
+
+    /**
+     * 手机号
+     */
+    private String phone;
+
+    /**
+     * 微信OpenID
+     */
+    private String wechatOpenid;
+
+    /**
+     * 微信UnionID
+     */
+    private String wechatUnionid;
+
+    /**
+     * 授权客户ID
+     */
+    private String authCustomerId;
+
+    /**
+     * 头像OSS ID
+     */
+    private Long avatar;
+
+    /**
+     * 删除标志(0代表存在 1代表删除)
+     */
+    @TableLogic
+    private String delFlag;
+
+}

+ 42 - 0
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/bo/SysAgreementBo.java

@@ -0,0 +1,42 @@
+package org.dromara.system.domain.bo;
+
+import io.github.linpeilie.annotations.AutoMapper;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.core.validate.EditGroup;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+import org.dromara.system.domain.SysAgreement;
+
+/**
+ * 协议信息业务对象 sys_agreement
+ *
+ * @author Lion Li
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@AutoMapper(target = SysAgreement.class, reverseConvertGenerate = false)
+public class SysAgreementBo extends BaseEntity {
+
+    /**
+     * 协议ID(修改时必填)
+     */
+    @NotNull(message = "协议ID不能为空", groups = { EditGroup.class })
+    private Long id;
+
+    /**
+     * 协议标题
+     */
+    @NotBlank(message = "协议标题不能为空")
+    @Size(min = 1, max = 100, message = "协议标题长度不能超过{max}个字符")
+    private String title;
+
+    /**
+     * 协议内容(前端传入 base64 编码的 HTML 富文本)
+     */
+    @NotBlank(message = "协议内容不能为空")
+    private String content;
+
+}

+ 63 - 0
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/bo/SysCustomerBo.java

@@ -0,0 +1,63 @@
+package org.dromara.system.domain.bo;
+
+import io.github.linpeilie.annotations.AutoMapper;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+import org.dromara.system.domain.SysCustomer;
+
+/**
+ * 客户业务对象 sys_customer
+ *
+ * @author Lion Li
+ */
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@AutoMapper(target = SysCustomer.class, reverseConvertGenerate = false)
+public class SysCustomerBo extends BaseEntity {
+
+    /**
+     * 客户ID
+     */
+    private Long id;
+
+    /**
+     * 用户名
+     */
+    @NotBlank(message = "用户名不能为空")
+    @Size(min = 0, max = 30, message = "用户名长度不能超过{max}个字符")
+    private String userName;
+
+    /**
+     * 手机号
+     */
+    @Size(min = 0, max = 11, message = "手机号长度不能超过{max}个字符")
+    private String phone;
+
+    /**
+     * 微信OpenID
+     */
+    @Size(min = 0, max = 255, message = "微信OpenID长度不能超过{max}个字符")
+    private String wechatOpenid;
+
+    /**
+     * 微信UnionID
+     */
+    @Size(min = 0, max = 255, message = "微信UnionID长度不能超过{max}个字符")
+    private String wechatUnionid;
+
+    /**
+     * 授权客户ID
+     */
+    @Size(min = 0, max = 255, message = "授权客户ID长度不能超过{max}个字符")
+    private String authCustomerId;
+
+    /**
+     * 头像OSS ID
+     */
+    private Long avatar;
+
+}

+ 43 - 0
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysAgreementVo.java

@@ -0,0 +1,43 @@
+package org.dromara.system.domain.vo;
+
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+import org.dromara.system.domain.SysAgreement;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 协议信息视图对象 sys_agreement
+ *
+ * @author Lion Li
+ */
+@Data
+@AutoMapper(target = SysAgreement.class)
+public class SysAgreementVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 协议ID
+     */
+    private Long id;
+
+    /**
+     * 协议标题
+     */
+    private String title;
+
+    /**
+     * 协议内容(明文 HTML 富文本)
+     */
+    private String content;
+
+    /**
+     * 更新时间
+     */
+    private Date updateTime;
+
+}

+ 74 - 0
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysCustomerVo.java

@@ -0,0 +1,74 @@
+package org.dromara.system.domain.vo;
+
+import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
+import cn.idev.excel.annotation.ExcelProperty;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+import org.dromara.system.domain.SysCustomer;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 客户视图对象 sys_customer
+ *
+ * @author Lion Li
+ */
+@Data
+@ExcelIgnoreUnannotated
+@AutoMapper(target = SysCustomer.class)
+public class SysCustomerVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 客户ID
+     */
+    @ExcelProperty(value = "客户ID")
+    private Long id;
+
+    /**
+     * 用户名
+     */
+    @ExcelProperty(value = "用户名")
+    private String userName;
+
+    /**
+     * 手机号
+     */
+    @ExcelProperty(value = "手机号")
+    private String phone;
+
+    /**
+     * 微信OpenID
+     */
+    @ExcelProperty(value = "微信OpenID")
+    private String wechatOpenid;
+
+    /**
+     * 微信UnionID
+     */
+    @ExcelProperty(value = "微信UnionID")
+    private String wechatUnionid;
+
+    /**
+     * 授权客户ID
+     */
+    @ExcelProperty(value = "授权客户ID")
+    private String authCustomerId;
+
+    /**
+     * 头像OSS ID
+     */
+    @ExcelProperty(value = "头像OSS ID")
+    private Long avatar;
+
+    /**
+     * 创建时间
+     */
+    @ExcelProperty(value = "创建时间")
+    private Date createTime;
+
+}

+ 13 - 0
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysAgreementMapper.java

@@ -0,0 +1,13 @@
+package org.dromara.system.mapper;
+
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+import org.dromara.system.domain.SysAgreement;
+import org.dromara.system.domain.vo.SysAgreementVo;
+
+/**
+ * 协议信息 Mapper 接口
+ *
+ * @author Lion Li
+ */
+public interface SysAgreementMapper extends BaseMapperPlus<SysAgreement, SysAgreementVo> {
+}

+ 14 - 0
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/mapper/SysCustomerMapper.java

@@ -0,0 +1,14 @@
+package org.dromara.system.mapper;
+
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+import org.dromara.system.domain.SysCustomer;
+import org.dromara.system.domain.vo.SysCustomerVo;
+
+/**
+ * 客户管理 数据层
+ *
+ * @author Lion Li
+ */
+public interface SysCustomerMapper extends BaseMapperPlus<SysCustomer, SysCustomerVo> {
+
+}

+ 23 - 0
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysAgreementService.java

@@ -0,0 +1,23 @@
+package org.dromara.system.service;
+
+import org.dromara.system.domain.bo.SysAgreementBo;
+import org.dromara.system.domain.vo.SysAgreementVo;
+
+/**
+ * 协议信息 Service 接口
+ *
+ * @author Lion Li
+ */
+public interface ISysAgreementService {
+
+    /**
+     * 根据ID查询协议(返回明文内容)
+     */
+    SysAgreementVo queryById(Long id);
+
+    /**
+     * 修改协议(接收 base64 编码内容,解码后存库,返回最新 VO 用于刷新缓存)
+     */
+    SysAgreementVo updateByBo(SysAgreementBo bo);
+
+}

+ 46 - 0
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysCustomerService.java

@@ -0,0 +1,46 @@
+package org.dromara.system.service;
+
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.system.domain.bo.SysCustomerBo;
+import org.dromara.system.domain.vo.SysCustomerVo;
+
+import java.util.Collection;
+
+/**
+ * 客户管理 Service接口
+ *
+ * @author Lion Li
+ */
+public interface ISysCustomerService {
+
+    /**
+     * 查询客户
+     */
+    SysCustomerVo queryById(Long id);
+
+    /**
+     * 分页查询客户列表
+     */
+    TableDataInfo<SysCustomerVo> queryPageList(SysCustomerBo bo, PageQuery pageQuery);
+
+    /**
+     * 修改客户
+     */
+    Boolean updateByBo(SysCustomerBo bo);
+
+    /**
+     * 校验并批量删除客户信息
+     */
+    Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
+
+    /**
+     * 根据微信UnionID查询客户
+     */
+    SysCustomerVo queryByWechatUnionid(String wechatUnionid);
+
+    /**
+     * 根据微信OpenID查询客户
+     */
+    SysCustomerVo queryByWechatOpenid(String wechatOpenid);
+}

+ 51 - 0
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysAgreementServiceImpl.java

@@ -0,0 +1,51 @@
+package org.dromara.system.service.impl;
+
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.constant.CacheNames;
+import org.dromara.common.core.utils.MapstructUtils;
+import org.springframework.cache.annotation.CachePut;
+import org.springframework.cache.annotation.Cacheable;
+import org.dromara.system.domain.SysAgreement;
+import org.dromara.system.domain.bo.SysAgreementBo;
+import org.dromara.system.domain.vo.SysAgreementVo;
+import org.dromara.system.mapper.SysAgreementMapper;
+import org.dromara.system.service.ISysAgreementService;
+import org.springframework.stereotype.Service;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+/**
+ * 协议信息 Service 业务层处理
+ *
+ * @author Lion Li
+ */
+@RequiredArgsConstructor
+@Service
+public class SysAgreementServiceImpl implements ISysAgreementService {
+
+    private final SysAgreementMapper baseMapper;
+
+    /**
+     * 根据ID查询协议,返回明文内容(走缓存)
+     */
+    @Cacheable(cacheNames = CacheNames.SYS_AGREEMENT, key = "#id")
+    @Override
+    public SysAgreementVo queryById(Long id) {
+        return baseMapper.selectVoById(id);
+    }
+
+    /**
+     * 修改协议:解码 base64 存库,刷新缓存并返回最新 VO
+     */
+    @CachePut(cacheNames = CacheNames.SYS_AGREEMENT, key = "#bo.id")
+    @Override
+    public SysAgreementVo updateByBo(SysAgreementBo bo) {
+        SysAgreement update = MapstructUtils.convert(bo, SysAgreement.class);
+        String decoded = new String(Base64.getDecoder().decode(bo.getContent()), StandardCharsets.UTF_8);
+        update.setContent(decoded);
+        baseMapper.updateById(update);
+        return baseMapper.selectVoById(bo.getId());
+    }
+
+}

+ 90 - 0
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysCustomerServiceImpl.java

@@ -0,0 +1,90 @@
+package org.dromara.system.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.utils.MapstructUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.system.domain.SysCustomer;
+import org.dromara.system.domain.bo.SysCustomerBo;
+import org.dromara.system.domain.vo.SysCustomerVo;
+import org.dromara.system.mapper.SysCustomerMapper;
+import org.dromara.system.service.ISysCustomerService;
+import org.springframework.stereotype.Service;
+
+import java.util.Collection;
+
+/**
+ * 客户管理 Service业务层处理
+ *
+ * @author Lion Li
+ */
+@RequiredArgsConstructor
+@Service
+public class SysCustomerServiceImpl implements ISysCustomerService {
+
+    private final SysCustomerMapper baseMapper;
+
+    /**
+     * 查询客户
+     */
+    @Override
+    public SysCustomerVo queryById(Long id) {
+        return baseMapper.selectVoById(id);
+    }
+
+    /**
+     * 分页查询客户列表
+     */
+    @Override
+    public TableDataInfo<SysCustomerVo> queryPageList(SysCustomerBo bo, PageQuery pageQuery) {
+        LambdaQueryWrapper<SysCustomer> lqw = buildQueryWrapper(bo);
+        Page<SysCustomerVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
+        return TableDataInfo.build(result);
+    }
+
+    private LambdaQueryWrapper<SysCustomer> buildQueryWrapper(SysCustomerBo bo) {
+        LambdaQueryWrapper<SysCustomer> lqw = Wrappers.lambdaQuery();
+        lqw.like(StringUtils.isNotBlank(bo.getUserName()), SysCustomer::getUserName, bo.getUserName());
+        lqw.eq(StringUtils.isNotBlank(bo.getPhone()), SysCustomer::getPhone, bo.getPhone());
+        lqw.eq(StringUtils.isNotBlank(bo.getWechatOpenid()), SysCustomer::getWechatOpenid, bo.getWechatOpenid());
+        lqw.orderByDesc(SysCustomer::getId);
+        return lqw;
+    }
+
+    /**
+     * 修改客户
+     */
+    @Override
+    public Boolean updateByBo(SysCustomerBo bo) {
+        SysCustomer update = MapstructUtils.convert(bo, SysCustomer.class);
+        return baseMapper.updateById(update) > 0;
+    }
+
+    /**
+     * 批量删除客户
+     */
+    @Override
+    public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
+        return baseMapper.deleteByIds(ids) > 0;
+    }
+
+    @Override
+    public SysCustomerVo queryByWechatUnionid(String wechatUnionid) {
+        return baseMapper.selectVoOne(
+            Wrappers.lambdaQuery(SysCustomer.class)
+                .eq(SysCustomer::getWechatUnionid, wechatUnionid)
+        );
+    }
+
+    @Override
+    public SysCustomerVo queryByWechatOpenid(String wechatOpenid) {
+        return baseMapper.selectVoOne(
+            Wrappers.lambdaQuery(SysCustomer.class)
+                .eq(SysCustomer::getWechatOpenid, wechatOpenid)
+        );
+    }
+}

+ 142 - 0
script/sql/sqlserver/v1/create.sql

@@ -0,0 +1,142 @@
+-- ----------------------------
+-- 协议信息表
+-- ----------------------------
+create table sys_agreement
+(
+    id          bigint        NOT NULL,
+    title       nvarchar(100) NOT NULL,
+    content     nvarchar(MAX) DEFAULT ('') NULL,
+    create_dept bigint,
+    create_by   bigint,
+    create_time datetime2(7),
+    update_by   bigint,
+    update_time datetime2(7),
+    CONSTRAINT PK__sys_agreement__id PRIMARY KEY CLUSTERED (id)
+    WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)
+    ON [PRIMARY]
+)
+ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
+GO
+
+EXEC sys.sp_addextendedproperty 'MS_Description', N'协议ID', 'SCHEMA', N'dbo', 'TABLE', N'sys_agreement', 'COLUMN', N'id'
+GO
+EXEC sys.sp_addextendedproperty 'MS_Description', N'协议标题', 'SCHEMA', N'dbo', 'TABLE', N'sys_agreement', 'COLUMN', N'title'
+GO
+EXEC sys.sp_addextendedproperty 'MS_Description', N'协议内容(富文本HTML)', 'SCHEMA', N'dbo', 'TABLE', N'sys_agreement', 'COLUMN', N'content'
+GO
+
+-- ----------------------------
+-- 客户表
+-- ----------------------------
+create table sys_customer
+(
+    id                 bigint            NOT NULL,
+    user_name          nvarchar(30)      NOT NULL,
+    phone              nvarchar(11)      DEFAULT ('') NULL,
+    wechat_openid      nvarchar(255)     DEFAULT ('') NULL,
+    wechat_unionid     nvarchar(255)     DEFAULT ('') NULL,
+    auth_customer_id   nvarchar(255)     DEFAULT ('') NULL,
+    avatar             bigint            DEFAULT (NULL) NULL,
+    tenant_id          nvarchar(20)      DEFAULT ('000000') NULL,
+    create_dept        bigint,
+    create_by          bigint,
+    create_time        datetime2(7),
+    update_by          bigint,
+    update_time        datetime2(7),
+    del_flag           nchar             DEFAULT ('0') NULL,
+    CONSTRAINT PK__sys_customer__id PRIMARY KEY CLUSTERED (id)
+    WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)
+    ON [PRIMARY]
+)
+ON [PRIMARY]
+GO
+
+EXEC sys.sp_addextendedproperty
+    'MS_Description', N'客户ID' ,
+    'SCHEMA', N'dbo',
+    'TABLE', N'sys_customer',
+    'COLUMN', N'id'
+GO
+EXEC sys.sp_addextendedproperty
+    'MS_Description', N'用户名' ,
+    'SCHEMA', N'dbo',
+    'TABLE', N'sys_customer',
+    'COLUMN', N'user_name'
+GO
+EXEC sys.sp_addextendedproperty
+    'MS_Description', N'手机号' ,
+    'SCHEMA', N'dbo',
+    'TABLE', N'sys_customer',
+    'COLUMN', N'phone'
+GO
+EXEC sys.sp_addextendedproperty
+    'MS_Description', N'微信OpenID' ,
+    'SCHEMA', N'dbo',
+    'TABLE', N'sys_customer',
+    'COLUMN', N'wechat_openid'
+GO
+EXEC sys.sp_addextendedproperty
+    'MS_Description', N'微信UnionID' ,
+    'SCHEMA', N'dbo',
+    'TABLE', N'sys_customer',
+    'COLUMN', N'wechat_unionid'
+GO
+EXEC sys.sp_addextendedproperty
+    'MS_Description', N'授权客户ID' ,
+    'SCHEMA', N'dbo',
+    'TABLE', N'sys_customer',
+    'COLUMN', N'auth_customer_id'
+GO
+EXEC sys.sp_addextendedproperty
+    'MS_Description', N'头像OSS ID' ,
+    'SCHEMA', N'dbo',
+    'TABLE', N'sys_customer',
+    'COLUMN', N'avatar'
+GO
+EXEC sys.sp_addextendedproperty
+    'MS_Description', N'租户ID' ,
+    'SCHEMA', N'dbo',
+    'TABLE', N'sys_customer',
+    'COLUMN', N'tenant_id'
+GO
+EXEC sys.sp_addextendedproperty
+    'MS_Description', N'创建部门' ,
+    'SCHEMA', N'dbo',
+    'TABLE', N'sys_customer',
+    'COLUMN', N'create_dept'
+GO
+EXEC sys.sp_addextendedproperty
+    'MS_Description', N'创建者' ,
+    'SCHEMA', N'dbo',
+    'TABLE', N'sys_customer',
+    'COLUMN', N'create_by'
+GO
+EXEC sys.sp_addextendedproperty
+    'MS_Description', N'创建时间' ,
+    'SCHEMA', N'dbo',
+    'TABLE', N'sys_customer',
+    'COLUMN', N'create_time'
+GO
+EXEC sys.sp_addextendedproperty
+    'MS_Description', N'更新者' ,
+    'SCHEMA', N'dbo',
+    'TABLE', N'sys_customer',
+    'COLUMN', N'update_by'
+GO
+EXEC sys.sp_addextendedproperty
+    'MS_Description', N'更新时间' ,
+    'SCHEMA', N'dbo',
+    'TABLE', N'sys_customer',
+    'COLUMN', N'update_time'
+GO
+EXEC sys.sp_addextendedproperty
+    'MS_Description', N'删除标志(0代表存在 1代表删除)' ,
+    'SCHEMA', N'dbo',
+    'TABLE', N'sys_customer',
+    'COLUMN', N'del_flag'
+GO
+EXEC sys.sp_addextendedproperty
+    'MS_Description', N'客户表' ,
+    'SCHEMA', N'dbo',
+    'TABLE', N'sys_customer'
+GO

+ 8 - 0
script/sql/sqlserver/v1/update.sql

@@ -0,0 +1,8 @@
+-- ----------------------------
+-- sys_agreement 初始数据
+-- ----------------------------
+INSERT INTO [dbo].[sys_agreement] ([id], [title], [content], [create_by], [create_time], [update_by], [update_time])
+VALUES (1, N'用户协议', N'<p>欢迎使用本系统,请仔细阅读以下用户协议...</p>', 1, GETDATE(), 1, GETDATE());
+INSERT INTO [dbo].[sys_agreement] ([id], [title], [content], [create_by], [create_time], [update_by], [update_time])
+VALUES (2, N'隐私政策', N'<p>我们非常重视您的个人隐私保护,请仔细阅读以下隐私政策...</p>', 1, GETDATE(), 1, GETDATE());
+GO