scopes;
+
+ private boolean ignoreCheckState;
+
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/config/AuthSource.java b/sf-oauth/src/main/java/com/sf/oauth/config/AuthSource.java
new file mode 100644
index 0000000..15e6412
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/config/AuthSource.java
@@ -0,0 +1,76 @@
+package com.sf.oauth.config;
+
+import com.sf.oauth.exception.AuthException;
+import com.sf.oauth.request.AuthDefaultRequest;
+
+/**
+ * OAuth平台的API地址的统一接口,提供以下方法:
+ * 1) {@link AuthSource#authorize()}: 获取授权url. 必须实现
+ * 2) {@link AuthSource#accessToken()}: 获取accessToken的url. 必须实现
+ * 3) {@link AuthSource#userInfo()}: 获取用户信息的url. 必须实现
+ * 4) {@link AuthSource#revoke()}: 获取取消授权的url. 非必须实现接口(部分平台不支持)
+ * 5) {@link AuthSource#refresh()}: 获取刷新授权的url. 非必须实现接口(部分平台不支持)
+ *
+ *
+ * @author zoukun
+ */
+public interface AuthSource {
+
+ /**
+ * 授权的api
+ *
+ * @return url
+ */
+ String authorize();
+
+ /**
+ * 获取accessToken的api
+ *
+ * @return url
+ */
+ String accessToken();
+
+ /**
+ * 获取用户信息的api
+ *
+ * @return url
+ */
+ String userInfo();
+
+ /**
+ * 取消授权的api
+ *
+ * @return url
+ */
+ default String revoke() {
+ throw new AuthException("UNSUPPORTED");
+ }
+
+ /**
+ * 刷新授权的api
+ *
+ * @return url
+ */
+ default String refresh() {
+ throw new AuthException("UNSUPPORTED");
+ }
+
+ /**
+ * 获取Source的字符串名字
+ *
+ * @return name
+ */
+ default String getName() {
+ if (this instanceof Enum) {
+ return String.valueOf(this);
+ }
+ return this.getClass().getSimpleName();
+ }
+
+ /**
+ * 平台对应的 AuthRequest 实现类,必须继承自 {@link AuthDefaultRequest}
+ *
+ * @return class
+ */
+ Class extends AuthDefaultRequest> getTargetClass();
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/controller/RestAuthController.java b/sf-oauth/src/main/java/com/sf/oauth/controller/RestAuthController.java
new file mode 100644
index 0000000..036574c
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/controller/RestAuthController.java
@@ -0,0 +1,143 @@
+package com.sf.oauth.controller;
+
+import cn.hutool.core.lang.UUID;
+import cn.hutool.core.util.StrUtil;
+import com.alibaba.fastjson2.JSONObject;
+import com.sf.common.constant.Constants;
+import com.sf.common.constant.HttpStatus;
+import com.sf.common.core.domain.AjaxResult;
+import com.sf.oauth.config.AuthConfig;
+import com.sf.oauth.domain.AuthCallback;
+import com.sf.oauth.domain.AuthUser;
+import com.sf.oauth.enums.scope.AuthHuaweiScope;
+import com.sf.oauth.exception.AuthException;
+import com.sf.oauth.request.AuthHuaweiRequest;
+import com.sf.oauth.request.AuthRequest;
+import com.sf.oauth.service.IAuthService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import java.io.IOException;
+import java.util.Arrays;
+
+import static org.apache.commons.lang3.StringUtils.EMPTY;
+
+/**
+ * @author zoukun
+ */
+@Slf4j
+@RestController
+@RequestMapping("/oauth")
+public class RestAuthController {
+
+ @Autowired
+ private IAuthService authService;
+
+ @GetMapping("/authorize/{source}")
+ public void authorize(@PathVariable("source") String source, HttpServletResponse response) throws IOException {
+ log.info("进入render:" + source);
+ AuthRequest authRequest = getAuthRequest(source, EMPTY, EMPTY);
+ String authorizeUrl = authRequest.authorize(UUID.fastUUID().toString());
+ log.info(authorizeUrl);
+ response.sendRedirect(authorizeUrl);
+ }
+
+ /**
+ * oauth平台中配置的授权回调地址
+ * 如在创建华为授权应用时的回调地址为:http://127.0.0.1:8080/oauth/callback/huawei
+ */
+ @RequestMapping("/callback/{source}")
+ public AjaxResult callback(@PathVariable("source") String source, @RequestBody AuthCallback callback, HttpSession session) {
+ log.info("进入callback:" + source + " callback params:" + JSONObject.toJSONString(callback));
+ AuthRequest authRequest = getAuthRequest(source, callback.getClientId(), callback.getClientSecret());
+ AuthUser authUser = authRequest.callback(callback);
+ log.info(JSONObject.toJSONString(authUser));
+ AjaxResult ajax = AjaxResult.success();
+ String token = authService.authLogin(authUser,session);
+ ajax.put(Constants.TOKEN, token);
+ return ajax;
+ }
+
+ /*
+ @RequestMapping("/revoke/{source}/{userId}")
+ @ResponseBody
+ public AjaxResult revokeAuth(@PathVariable("source") String source, @PathVariable("userId") String userId) throws IOException {
+ AuthRequest authRequest = getAuthRequest(source.toLowerCase());
+
+ AuthUser user = userService.getByUuid(uuid);
+ if (null == user) {
+ return Response.error("用户不存在");
+ }
+ AjaxResult response = null;
+ try {
+ response = authRequest.revoke(user.getToken());
+ if (response.ok()) {
+ userService.remove(user.getUuid());
+ return Response.success("用户 [" + user.getUsername() + "] 的 授权状态 已收回!");
+ }
+ return Response.error("用户 [" + user.getUsername() + "] 的 授权状态 收回失败!" + response.getMsg());
+ } catch (AuthException e) {
+ return Response.error(e.getErrorMsg());
+ }
+ }
+
+ @RequestMapping("/refresh/{source}/{userId}")
+ @ResponseBody
+ public Object refreshAuth(@PathVariable("source") String source, @PathVariable("userId") String userId) {
+ AuthRequest authRequest = getAuthRequest(source.toLowerCase());
+
+ AuthUser user = userService.getByUuid(userId);
+ if (null == user) {
+ return Response.error("用户不存在");
+ }
+ AjaxResult response = null;
+ try {
+ response = authRequest.refresh(user.getToken());
+ if (response.ok()) {
+ user.setToken(response.getData());
+ userService.save(user);
+ return Response.success("用户 [" + user.getUsername() + "] 的 access token 已刷新!新的 callback: " + response.getData().getAccessToken());
+ }
+ return Response.error("用户 [" + user.getUsername() + "] 的 access token 刷新失败!" + response.getMsg());
+ } catch (AuthException e) {
+ return Response.error(e.getErrorMsg());
+ }
+ }*/
+
+ /**
+ * 根据具体的授权来源,获取授权请求工具类
+ *
+ * @param source
+ * @return
+ */
+ private AuthRequest getAuthRequest(String source, String clientId, String clientSecret) {
+ AuthRequest authRequest = null;
+ switch (source.toLowerCase()) {
+ case "huawei":
+ authRequest = new AuthHuaweiRequest(AuthConfig.builder()
+ .clientId(StrUtil.isBlank(clientId) ? "110693217" : clientId)
+ .clientSecret(StrUtil.isBlank(clientSecret) ? "1410c01bc71c7ba587175ae79e500137c70945acc1416a38127cf98a09a6f8ba" : clientSecret)
+ .redirectUri("")
+ .ignoreCheckState(true)
+ .scopes(Arrays.asList(
+ AuthHuaweiScope.BASE_PROFILE.getScope(),
+ AuthHuaweiScope.MOBILE_NUMBER.getScope(),
+ AuthHuaweiScope.ACCOUNTLIST.getScope(),
+ AuthHuaweiScope.SCOPE_DRIVE_FILE.getScope(),
+ AuthHuaweiScope.SCOPE_DRIVE_APPDATA.getScope()
+ ))
+ .build());
+ break;
+ default:
+ break;
+ }
+ if (null == authRequest) {
+ throw new AuthException(HttpStatus.BAD_REQUEST, "未获取到有效的Auth配置");
+ }
+ return authRequest;
+ }
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/domain/AuthCallback.java b/sf-oauth/src/main/java/com/sf/oauth/domain/AuthCallback.java
new file mode 100644
index 0000000..403dfb8
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/domain/AuthCallback.java
@@ -0,0 +1,42 @@
+package com.sf.oauth.domain;
+
+import lombok.*;
+
+import java.io.Serializable;
+
+/**
+ * 授权回调时的参数类
+ *
+ * @author zk
+ */
+@Getter
+@Setter
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class AuthCallback implements Serializable {
+
+ /**
+ * 访问AuthorizeUrl后回调时带的参数code
+ */
+ private String code;
+
+ /**
+ * 访问AuthorizeUrl后回调时带的参数state,用于和请求AuthorizeUrl前的state比较,防止CSRF攻击
+ */
+ private String state;
+
+ /**
+ * 华为授权登录
+ *
+ * 客户端id:对应各平台的appKey
+ */
+ private String clientId;
+
+ /**
+ * 客户端Secret:对应各平台的appSecret
+ */
+ private String clientSecret;
+
+
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/domain/AuthToken.java b/sf-oauth/src/main/java/com/sf/oauth/domain/AuthToken.java
new file mode 100644
index 0000000..902bc1c
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/domain/AuthToken.java
@@ -0,0 +1,42 @@
+package com.sf.oauth.domain;
+
+import lombok.*;
+
+import java.io.Serializable;
+
+/**
+ * 授权所需的token
+ *
+ * @author zoukun
+ */
+@Getter
+@Setter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AuthToken implements Serializable {
+ private String accessToken;
+ private int expireIn;
+ private String refreshToken;
+ private int refreshTokenExpireIn;
+ private String uid;
+ private String openId;
+ private String accessCode;
+ private String unionId;
+
+ /**
+ * 华为返回 生成的Access Token中包含的scope。
+ */
+ private String scope;
+
+ /**
+ * 华为返回 固定返回Bearer,标识返回Access Token的类型
+ */
+ private String tokenType;
+
+ /**
+ * 华为返回 返回JWT格式数据,包含用户基本帐号、用户邮箱等信息。
+ * 参照https://developer.huawei.com/consumer/cn/doc/HMSCore-References/account-verify-id-token_hms_reference-0000001050050577#section3142132691914
+ */
+ private String idToken;
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/domain/AuthUser.java b/sf-oauth/src/main/java/com/sf/oauth/domain/AuthUser.java
new file mode 100644
index 0000000..1201e16
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/domain/AuthUser.java
@@ -0,0 +1,79 @@
+package com.sf.oauth.domain;
+
+
+import com.alibaba.fastjson2.JSONObject;
+import com.sf.oauth.enums.AuthUserGender;
+import lombok.*;
+
+import java.io.Serializable;
+
+/**
+ * 授权成功后的用户信息,根据授权平台的不同,获取的数据完整性也不同
+ *
+ * @author zoukun
+ */
+@Getter
+@Setter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AuthUser implements Serializable {
+ /**
+ * 用户第三方系统的唯一id。
+ */
+ private String uuid;
+ /**
+ * 用户名
+ */
+ private String username;
+ /**
+ * 用户昵称
+ */
+ private String nickname;
+ /**
+ * 用户头像
+ */
+ private String avatar;
+ /**
+ * 用户网址
+ */
+ private String blog;
+ /**
+ * 所在公司
+ */
+ private String company;
+ /**
+ * 位置
+ */
+ private String location;
+ /**
+ * 用户邮箱
+ */
+ private String email;
+ /**
+ * 用户手机号
+ */
+ private String mobileNumber;
+ /**
+ * 用户备注(各平台中的用户个人介绍)
+ */
+ private String remark;
+ /**
+ * 性别
+ */
+ private AuthUserGender gender;
+ /**
+ * 用户来源
+ */
+ private String source;
+ /**
+ * 用户授权的token信息
+ */
+ private AuthToken token;
+ /**
+ * 第三方平台返回的原始用户信息
+ */
+ private JSONObject rawUserInfo;
+
+
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/enums/AuthDefaultSource.java b/sf-oauth/src/main/java/com/sf/oauth/enums/AuthDefaultSource.java
new file mode 100644
index 0000000..789a4a0
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/enums/AuthDefaultSource.java
@@ -0,0 +1,47 @@
+package com.sf.oauth.enums;
+
+import com.sf.oauth.config.AuthSource;
+import com.sf.oauth.request.AuthDefaultRequest;
+import com.sf.oauth.request.AuthHuaweiRequest;
+
+/**
+ * 内置的各api需要的url, 用枚举类分平台类型管理
+ *
+ * @author zoukun
+ */
+public enum AuthDefaultSource implements AuthSource {
+
+
+ /**
+ * 华为oauth2/v3
+ */
+ HUAWEI {
+ @Override
+ public String authorize() {
+ return "https://oauth-login.cloud.huawei.com/oauth2/v3/authorize";
+ }
+
+ @Override
+ public String accessToken() {
+ return "https://oauth-login.cloud.huawei.com/oauth2/v3/token";
+ }
+
+ @Override
+ public String userInfo() {
+ return "https://account.cloud.huawei.com/rest.php";
+ }
+
+ @Override
+ public String refresh() {
+ return "https://oauth-login.cloud.huawei.com/oauth2/v3/token";
+ }
+
+ @Override
+ public Class extends AuthDefaultRequest> getTargetClass() {
+ return AuthHuaweiRequest.class;
+ }
+ },
+
+
+
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/enums/AuthPlatformInfo.java b/sf-oauth/src/main/java/com/sf/oauth/enums/AuthPlatformInfo.java
new file mode 100644
index 0000000..ae1aa39
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/enums/AuthPlatformInfo.java
@@ -0,0 +1,51 @@
+package com.sf.oauth.enums;
+
+import java.util.*;
+
+/**
+ * @author zoukun
+ */
+public enum AuthPlatformInfo {
+
+ /**
+ * 平台
+ */
+ HUAWEI("华为", "huawei", "https://developer.huawei.com/consumer/cn/doc/HMSCore-Guides/open-platform-oauth-0000001053629189"),
+ ;
+
+ /**
+ * 平台名
+ */
+ private final String name;
+ /**
+ * 平台编码
+ */
+ private final String code;
+ /**
+ * 官网api文档
+ */
+ private final String apiDoc;
+
+ AuthPlatformInfo(String name, String code, String apiDoc) {
+ this.name = name;
+ this.code = code;
+ this.apiDoc = apiDoc;
+ }
+
+ public static List getPlatformInfos() {
+ return Arrays.asList(AuthPlatformInfo.values());
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getCode() {
+ return code;
+ }
+
+ public String getApiDoc() {
+ return apiDoc;
+ }
+
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/enums/AuthUserGender.java b/sf-oauth/src/main/java/com/sf/oauth/enums/AuthUserGender.java
new file mode 100644
index 0000000..7a76bf3
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/enums/AuthUserGender.java
@@ -0,0 +1,45 @@
+package com.sf.oauth.enums;
+
+import cn.hutool.core.util.StrUtil;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 用户性别
+ *
+ * @author zoukun
+ */
+@Getter
+@AllArgsConstructor
+public enum AuthUserGender {
+ /**
+ * MALE/FAMALE为正常值,通过{@link AuthUserGender#getRealGender(String)}方法获取真实的性别
+ * UNKNOWN为容错值,部分平台不会返回用户性别,为了方便统一,使用UNKNOWN标记所有未知或不可测的用户性别信息
+ */
+ MALE("0", "男"),
+ FEMALE("1", "女"),
+ UNKNOWN("2", "未知");
+
+ private String code;
+ private String desc;
+
+ /**
+ * 获取用户的实际性别,常规网站
+ *
+ * @param originalGender 用户第三方标注的原始性别
+ * @return 用户性别
+ */
+ public static AuthUserGender getRealGender(String originalGender) {
+ if (null == originalGender || UNKNOWN.getCode().equals(originalGender)) {
+ return UNKNOWN;
+ }
+ String[] males = {"m", "男", "0", "male"};
+ if (Arrays.asList(males).contains(originalGender.toLowerCase())) {
+ return MALE;
+ }
+ return FEMALE;
+ }
+
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/enums/scope/AuthHuaweiScope.java b/sf-oauth/src/main/java/com/sf/oauth/enums/scope/AuthHuaweiScope.java
new file mode 100644
index 0000000..4d61bb0
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/enums/scope/AuthHuaweiScope.java
@@ -0,0 +1,44 @@
+package com.sf.oauth.enums.scope;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 华为平台 OAuth 授权范围
+ *
+ * @author zoukun
+ */
+@Getter
+@AllArgsConstructor
+public enum AuthHuaweiScope implements AuthScope {
+
+ /**
+ * {@code scope} 含义,以{@code description} 为准
+ */
+ BASE_PROFILE("https://www.huawei.com/auth/account/base.profile", "获取用户的基本信息", true),
+ MOBILE_NUMBER("https://www.huawei.com/auth/account/mobile.number", "获取用户的手机号", false),
+ ACCOUNTLIST("https://www.huawei.com/auth/account/accountlist", "获取用户的账单列表", false),
+
+ /**
+ * 以下两个 scope 不需要经过华为评估和验证
+ */
+ SCOPE_DRIVE_FILE("https://www.huawei.com/auth/drive.file", "只允许访问由应用程序创建或打开的文件", false),
+ SCOPE_DRIVE_APPDATA("https://www.huawei.com/auth/drive.appdata", "只允许访问由应用程序创建或打开的文件", false),
+ /**
+ * 以下四个 scope 使用前需要向drivekit@huawei.com提交申请
+ *
+ * 参考:https://developer.huawei.com/consumer/cn/doc/development/HMSCore-Guides-V5/server-dev-0000001050039664-V5#ZH-CN_TOPIC_0000001050039664__section1618418855716
+ */
+ SCOPE_DRIVE("https://www.huawei.com/auth/drive", "只允许访问由应用程序创建或打开的文件", false),
+ SCOPE_DRIVE_READONLY("https://www.huawei.com/auth/drive.readonly", "只允许访问由应用程序创建或打开的文件", false),
+ SCOPE_DRIVE_METADATA("https://www.huawei.com/auth/drive.metadata", "只允许访问由应用程序创建或打开的文件", false),
+ SCOPE_DRIVE_METADATA_READONLY("https://www.huawei.com/auth/drive.metadata.readonly", "只允许访问由应用程序创建或打开的文件", false),
+
+
+ ;
+
+ private final String scope;
+ private final String description;
+ private final boolean isDefault;
+
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/enums/scope/AuthScope.java b/sf-oauth/src/main/java/com/sf/oauth/enums/scope/AuthScope.java
new file mode 100644
index 0000000..52ef1d4
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/enums/scope/AuthScope.java
@@ -0,0 +1,23 @@
+package com.sf.oauth.enums.scope;
+
+/**
+ * 各个平台 scope 类的统一接口
+ *
+ * @author zoukun
+ */
+public interface AuthScope {
+
+ /**
+ * 获取字符串 {@code scope},对应为各平台实际使用的 {@code scope}
+ *
+ * @return String
+ */
+ String getScope();
+
+ /**
+ * 判断当前 {@code scope} 是否为各平台默认启用的
+ *
+ * @return boolean
+ */
+ boolean isDefault();
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/exception/AuthException.java b/sf-oauth/src/main/java/com/sf/oauth/exception/AuthException.java
new file mode 100644
index 0000000..b0f6959
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/exception/AuthException.java
@@ -0,0 +1,46 @@
+package com.sf.oauth.exception;
+
+
+import com.sf.oauth.config.AuthSource;
+
+/**
+ * 授权异常
+ *
+ * @author zoukun
+ */
+public class AuthException extends RuntimeException {
+
+ private int errorCode;
+ private String errorMsg;
+
+ public AuthException(String errorMsg) {
+ super(errorMsg);
+ this.errorMsg = errorMsg;
+ }
+
+ public AuthException(int errorCode, String errorMsg) {
+ super(errorMsg);
+ this.errorCode = errorCode;
+ this.errorMsg = errorMsg;
+ }
+
+ public AuthException(int errorCode, String errorMsg, AuthSource source) {
+ this(errorCode, String.format("%s [%s]", errorMsg, source.getName()));
+ }
+
+ public AuthException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public AuthException(Throwable cause) {
+ super(cause);
+ }
+
+ public int getErrorCode() {
+ return errorCode;
+ }
+
+ public String getErrorMsg() {
+ return errorMsg;
+ }
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/request/AuthDefaultRequest.java b/sf-oauth/src/main/java/com/sf/oauth/request/AuthDefaultRequest.java
new file mode 100644
index 0000000..6a9f991
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/request/AuthDefaultRequest.java
@@ -0,0 +1,177 @@
+package com.sf.oauth.request;
+
+import cn.hutool.core.lang.UUID;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.core.util.URLUtil;
+import cn.hutool.http.HttpUtil;
+import com.sf.common.constant.HttpStatus;
+import com.sf.oauth.config.AuthConfig;
+import com.sf.oauth.config.AuthSource;
+import com.sf.oauth.exception.AuthException;
+import com.sf.oauth.domain.AuthCallback;
+import com.sf.oauth.domain.AuthToken;
+import com.sf.oauth.domain.AuthUser;
+import com.sf.oauth.utils.AuthChecker;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 默认的request处理类
+ *
+ * @author zoukun
+ */
+@Slf4j
+public abstract class AuthDefaultRequest implements AuthRequest {
+ protected AuthConfig config;
+ protected AuthSource source;
+
+ public AuthDefaultRequest(AuthConfig config, AuthSource source) {
+ this.config = config;
+ this.source = source;
+ if (!AuthChecker.isSupportedAuth(config, source)) {
+ throw new AuthException(HttpStatus.ERROR,"Parameter incomplete", source);
+ }
+ // 校验配置合法性
+ // AuthChecker.checkConfig(config, source);
+ }
+
+ /**
+ * 获取access token
+ *
+ * @param authCallback 授权成功后的回调参数
+ * @return token
+ * @see AuthDefaultRequest#authorize()
+ * @see AuthDefaultRequest#authorize(String)
+ */
+ protected abstract AuthToken getAccessToken(AuthCallback authCallback);
+
+ /**
+ * 使用token换取用户信息
+ *
+ * @param authToken token信息
+ * @return 用户信息
+ * @see AuthDefaultRequest#getAccessToken(AuthCallback)
+ */
+ protected abstract AuthUser getUserInfo(AuthToken authToken);
+
+ /**
+ * 统一的登录入口。当通过{@link AuthDefaultRequest#authorize(String)}授权成功后,会跳转到调用方的相关回调方法中
+ * 方法的入参可以使用{@code AuthCallback},{@code AuthCallback}类中封装好了OAuth2授权回调所需要的参数
+ *
+ * @param authCallback 用于接收回调参数的实体
+ * @return AuthResponse
+ */
+ @Override
+ public AuthUser callback(AuthCallback authCallback) {
+ try {
+ checkCode(authCallback);
+ if (!config.isIgnoreCheckState()) {
+ AuthChecker.checkState(authCallback.getState(), source);
+ }
+
+ AuthToken authToken = this.getAccessToken(authCallback);
+ return this.getUserInfo(authToken);
+ } catch (Exception e) {
+ log.error("Failed to callback with oauth authorization", e);
+ throw new AuthException(HttpStatus.UNAUTHORIZED,"Failed to callback with oauth authorization");
+ }
+ }
+
+ protected void checkCode(AuthCallback authCallback) {
+ AuthChecker.checkCode(source, authCallback);
+ }
+
+
+ /**
+ * 返回授权url,可自行跳转页面
+ *
+ * 不建议使用该方式获取授权地址,不带{@code state}的授权地址,容易受到csrf攻击。
+ * 建议使用{@link AuthDefaultRequest#authorize(String)}方法生成授权地址,在回调方法中对{@code state}进行校验
+ *
+ * @return 返回授权地址
+ * @see AuthDefaultRequest#authorize(String)
+ */
+ @Override
+ public String authorize() {
+ return this.authorize(null);
+ }
+
+ /**
+ * 返回带{@code state}参数的授权url,授权回调时会带上这个{@code state}
+ *
+ * @param state state 验证授权流程的参数,可以防止csrf
+ * @return 返回授权地址
+ */
+ @Override
+ public String authorize(String state) {
+ Map form = new HashMap<>(8);
+ form.put("client_id", config.getClientId());
+ form.put("client_secret", config.getClientSecret());
+ form.put("redirect_uri", config.getRedirectUri());
+ form.put("state", getRealState(state));
+ form.put("response_type","code");
+ return HttpUtil.get(source.authorize(), form);
+ }
+
+ /**
+ * 返回获取accessToken的url
+ *
+ * @param code 授权码
+ * @return 返回获取accessToken的url
+ */
+ protected String accessTokenUrl(String code) {
+ Map form = new HashMap<>(8);
+ form.put("code", code);
+ form.put("client_id", config.getClientId());
+ form.put("client_secret", config.getClientSecret());
+ form.put("grant_type", "authorization_code");
+ form.put("redirect_uri", config.getRedirectUri());
+ return HttpUtil.get(source.accessToken(),form);
+ }
+
+
+
+
+ /**
+ * 获取state,如果为空, 则默认取当前日期的时间戳
+ *
+ * @param state 原始的state
+ * @return 返回不为null的state
+ */
+ protected String getRealState(String state) {
+ if (StrUtil.isEmpty(state)) {
+ state = UUID.fastUUID().toString();
+ }
+ // todo 需要缓存state
+ return state;
+ }
+
+
+ /**
+ * 获取以 {@code separator}分割过后的 scope 信息
+ *
+ * @param separator 多个 {@code scope} 间的分隔符
+ * @param encode 是否 encode 编码
+ * @param defaultScopes 默认的 scope, 当客户端没有配置 {@code scopes} 时启用
+ * @return String
+ */
+ protected String getScopes(String separator, boolean encode, List defaultScopes) {
+ List scopes = config.getScopes();
+ if (null == scopes || scopes.isEmpty()) {
+ if (null == defaultScopes || defaultScopes.isEmpty()) {
+ return "";
+ }
+ scopes = defaultScopes;
+ }
+ if (null == separator) {
+ // 默认为空格
+ separator = " ";
+ }
+ String scopeStr = String.join(separator, scopes);
+ return encode ? URLUtil.encode(scopeStr) : scopeStr;
+ }
+
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/request/AuthHuaweiRequest.java b/sf-oauth/src/main/java/com/sf/oauth/request/AuthHuaweiRequest.java
new file mode 100644
index 0000000..065caf7
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/request/AuthHuaweiRequest.java
@@ -0,0 +1,168 @@
+package com.sf.oauth.request;
+
+
+import cn.hutool.http.HttpUtil;
+import com.alibaba.fastjson2.JSONObject;
+import com.sf.common.constant.HttpStatus;
+import com.sf.common.core.domain.AjaxResult;
+import com.sf.oauth.config.AuthConfig;
+import com.sf.oauth.enums.AuthDefaultSource;
+import com.sf.oauth.enums.AuthUserGender;
+import com.sf.oauth.enums.scope.AuthHuaweiScope;
+import com.sf.oauth.exception.AuthException;
+import com.sf.oauth.domain.AuthCallback;
+import com.sf.oauth.domain.AuthToken;
+import com.sf.oauth.domain.AuthUser;
+import com.sf.oauth.utils.AuthScopeUtils;
+import io.jsonwebtoken.Jwts;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.HashMap;
+import java.util.Map;
+
+
+/**
+ * 华为授权登录
+ *
+ * @author zkun
+ */
+@Slf4j
+public class AuthHuaweiRequest extends AuthDefaultRequest {
+
+ public AuthHuaweiRequest(AuthConfig config) {
+ super(config, AuthDefaultSource.HUAWEI);
+ }
+
+
+ /**
+ * 获取access token
+ *
+ * @param authCallback 授权成功后的回调参数
+ * @return token
+ * @see AuthDefaultRequest#authorize()
+ * @see AuthDefaultRequest#authorize(String)
+ */
+ @Override
+ protected AuthToken getAccessToken(AuthCallback authCallback) {
+ Map form = new HashMap<>(8);
+ form.put("grant_type", "authorization_code");
+ form.put("code", authCallback.getCode());
+ form.put("client_id", config.getClientId());
+ form.put("client_secret", config.getClientSecret());
+ form.put("redirect_uri", config.getRedirectUri());
+ String response = HttpUtil.post(source.accessToken(), form);
+ return getAuthToken(response);
+ }
+
+ /**
+ * 使用token换取用户信息
+ *
+ * @param authToken token信息
+ * @return 用户信息
+ * @see AuthDefaultRequest#getAccessToken(AuthCallback)
+ */
+ @Override
+ protected AuthUser getUserInfo(AuthToken authToken) {
+ Map form = new HashMap<>(8);
+ form.put("nsp_ts", System.currentTimeMillis() + "");
+ form.put("access_token", authToken.getAccessToken());
+ form.put("nsp_svc", "GOpen.User.getInfo");
+ form.put("getNickName", "1");
+ String response = HttpUtil.post(source.userInfo(), form);
+ log.info(response);
+
+ JSONObject object = JSONObject.parseObject(response);
+ this.checkResponse(object);
+ AuthUserGender gender = getRealGender(object);
+ return AuthUser.builder()
+ .rawUserInfo(object)
+ .uuid(object.getString("unionID"))
+ .username(object.getString("displayName"))
+ .nickname(object.getString("displayName"))
+ .gender(gender)
+ .avatar(object.getString("headPictureURL"))
+ .mobileNumber(object.getString("mobileNumber"))
+ .email(object.getString("email"))
+ .token(authToken)
+ .source(source.toString())
+ .build();
+ }
+
+ /**
+ * 刷新access token (续期)
+ *
+ * @param authToken 登录成功后返回的Token信息
+ * @return AuthResponse
+ */
+ @Override
+ public AjaxResult refresh(AuthToken authToken) {
+ Map form = new HashMap<>(8);
+ form.put("client_id", config.getClientId());
+ form.put("client_secret", config.getClientSecret());
+ form.put("refresh_token", authToken.getRefreshToken());
+ form.put("grant_type", "refresh_token");
+ String response = HttpUtil.post(source.refresh(), form);
+ return AjaxResult.success(getAuthToken(response));
+ }
+
+ private AuthToken getAuthToken(String response) {
+ JSONObject object = JSONObject.parseObject(response);
+
+ this.checkResponse(object);
+
+ return AuthToken.builder()
+ .accessToken(object.getString("access_token"))
+ .expireIn(object.getIntValue("expires_in"))
+ .refreshToken(object.getString("refresh_token"))
+ .scope(object.getString("scope"))
+ .tokenType(object.getString("token_type"))
+ .idToken(object.getString("id_token"))
+ .build();
+ }
+
+ /**
+ * 返回带{@code state}参数的授权url,授权回调时会带上这个{@code state}
+ *
+ * @param state state 验证授权流程的参数,可以防止csrf
+ * @return 返回授权地址
+ */
+ @Override
+ public String authorize(String state) {
+ Map form = new HashMap<>(8);
+ form.put("client_id", config.getClientId());
+ form.put("client_secret", config.getClientSecret());
+ form.put("redirect_uri", config.getRedirectUri());
+ form.put("state", getRealState(state));
+ form.put("response_type", "code");
+ form.put("access_type", "offline");
+ form.put("scope", this.getScopes(" ", true, AuthScopeUtils.getDefaultScopes(AuthHuaweiScope.values())));
+ return HttpUtil.get(source.authorize(), form);
+ }
+
+
+ /**
+ * 获取用户的实际性别。华为系统中,用户的性别:1表示女,0表示男
+ *
+ * @param object obj
+ * @return AuthUserGender
+ */
+ private AuthUserGender getRealGender(JSONObject object) {
+ int genderCodeInt = object.getIntValue("gender");
+ String genderCode = genderCodeInt == 0 ? "0" : (genderCodeInt == 1) ? "1" : genderCodeInt + "";
+ return AuthUserGender.getRealGender(genderCode);
+ }
+
+ /**
+ * 校验响应结果
+ *
+ * @param object 接口返回的结果
+ */
+ private void checkResponse(JSONObject object) {
+ if (object.containsKey("NSP_STATUS")) {
+ throw new AuthException(object.getString("error"));
+ }
+ if (object.containsKey("error")) {
+ throw new AuthException(object.getString("sub_error") + ":" + object.getString("error_description"));
+ }
+ }
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/request/AuthRequest.java b/sf-oauth/src/main/java/com/sf/oauth/request/AuthRequest.java
new file mode 100644
index 0000000..5691774
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/request/AuthRequest.java
@@ -0,0 +1,76 @@
+package com.sf.oauth.request;
+
+
+import com.sf.common.constant.HttpStatus;
+import com.sf.common.core.domain.AjaxResult;
+import com.sf.oauth.domain.AuthUser;
+import com.sf.oauth.exception.AuthException;
+import com.sf.oauth.domain.AuthCallback;
+import com.sf.oauth.domain.AuthToken;
+
+/**
+ * {@code Request}公共接口,所有平台的{@code Request}都需要实现该接口
+ *
+ * {@link AuthRequest#authorize()}
+ * {@link AuthRequest#authorize(String)}
+ * {@link AuthRequest#callback(AuthCallback)}
+ * {@link AuthRequest#revoke(AuthToken)}
+ * {@link AuthRequest#refresh(AuthToken)}
+ *
+ * @author zoukun
+ */
+public interface AuthRequest {
+
+ /**
+ * 返回授权url,可自行跳转页面
+ *
+ * 不建议使用该方式获取授权地址,不带{@code state}的授权地址,容易受到csrf攻击。
+ * 建议使用{@link AuthDefaultRequest#authorize(String)}方法生成授权地址,在回调方法中对{@code state}进行校验
+ *
+ * @return 返回授权地址
+ */
+ @Deprecated
+ default String authorize() {
+ throw new AuthException(HttpStatus.UNAUTHORIZED,"Not Implemented");
+ }
+
+ /**
+ * 返回带{@code state}参数的授权url,授权回调时会带上这个{@code state}
+ *
+ * @param state state 验证授权流程的参数,可以防止csrf
+ * @return 返回授权地址
+ */
+ default String authorize(String state) {
+ throw new AuthException(HttpStatus.UNAUTHORIZED,"Not Implemented");
+ }
+
+ /**
+ * 第三方登录
+ *
+ * @param authCallback 用于接收回调参数的实体
+ * @return 返回登录成功后的用户信息
+ */
+ default AuthUser callback(AuthCallback authCallback) {
+ throw new AuthException(HttpStatus.UNAUTHORIZED,"Not Implemented");
+ }
+
+ /**
+ * 撤销授权
+ *
+ * @param authToken 登录成功后返回的Token信息
+ * @return AjaxResult
+ */
+ default AjaxResult revoke(AuthToken authToken) {
+ throw new AuthException(HttpStatus.UNAUTHORIZED,"Not Implemented");
+ }
+
+ /**
+ * 刷新access token (续期)
+ *
+ * @param authToken 登录成功后返回的Token信息
+ * @return AjaxResult
+ */
+ default AjaxResult refresh(AuthToken authToken) {
+ throw new AuthException(HttpStatus.UNAUTHORIZED,"Not Implemented");
+ }
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/service/IAuthService.java b/sf-oauth/src/main/java/com/sf/oauth/service/IAuthService.java
new file mode 100644
index 0000000..d86faa2
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/service/IAuthService.java
@@ -0,0 +1,15 @@
+package com.sf.oauth.service;
+
+import com.sf.oauth.domain.AuthUser;
+
+import javax.servlet.http.HttpSession;
+
+/**
+ * 功能描述:
+ *
+ * @author a_kun
+ * @date 2024/4/12 10:21
+ */
+public interface IAuthService {
+ String authLogin(AuthUser authUser, HttpSession session);
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/service/impl/IAuthServiceImpl.java b/sf-oauth/src/main/java/com/sf/oauth/service/impl/IAuthServiceImpl.java
new file mode 100644
index 0000000..82d2cc0
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/service/impl/IAuthServiceImpl.java
@@ -0,0 +1,62 @@
+package com.sf.oauth.service.impl;
+
+import com.sf.common.core.domain.AjaxResult;
+import com.sf.common.core.domain.entity.SysUser;
+import com.sf.common.enums.UserStatus;
+import com.sf.common.utils.SecurityUtils;
+import com.sf.common.utils.ip.IpUtils;
+import com.sf.framework.web.service.SysLoginService;
+import com.sf.oauth.domain.AuthUser;
+import com.sf.oauth.exception.AuthException;
+import com.sf.oauth.service.IAuthService;
+import com.sf.system.service.ISysUserService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import javax.servlet.http.HttpSession;
+import java.util.Date;
+
+/**
+ * 功能描述:
+ *
+ * @author a_kun
+ * @date 2024/4/12 10:21
+ */
+@Slf4j
+@Service
+public class IAuthServiceImpl implements IAuthService {
+
+ @Autowired
+ private SysLoginService loginService;
+
+ @Autowired
+ private ISysUserService userService;
+
+
+ @Override
+ public String authLogin(AuthUser authUser, HttpSession session) {
+ // TODO 应该要登录后绑定一个平台账号,没有平台账号要创建,保存用户的三方来源,及用户的三方唯一标识和绑定的用户
+ if (authUser != null) {
+ SysUser sysUser = new SysUser();
+ sysUser.setUserName(authUser.getUsername());
+ if (userService.checkUserNameUnique(sysUser)) {
+ // 注册用户后登录
+ sysUser.setNickName(authUser.getNickname());
+ sysUser.setEmail(authUser.getEmail());
+ sysUser.setPhonenumber(authUser.getMobileNumber());
+ sysUser.setSex(authUser.getGender().getCode());
+ sysUser.setAvatar(authUser.getAvatar());
+ sysUser.setPassword(SecurityUtils.encryptPassword("ztzh@sac123"));
+ sysUser.setStatus(UserStatus.OK.getCode());
+ sysUser.setDelFlag("0");
+ sysUser.setLoginIp(IpUtils.getHostIp());
+ sysUser.setLoginDate(new Date());
+ userService.insertUser(sysUser);
+ }
+ // 生成令牌
+ return loginService.noPwdLogin(authUser.getUsername(), session);
+ }
+ throw new AuthException("login error");
+ }
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/utils/AuthChecker.java b/sf-oauth/src/main/java/com/sf/oauth/utils/AuthChecker.java
new file mode 100644
index 0000000..9fe8961
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/utils/AuthChecker.java
@@ -0,0 +1,79 @@
+package com.sf.oauth.utils;
+
+
+import cn.hutool.core.util.StrUtil;
+import com.sf.common.constant.HttpStatus;
+import com.sf.oauth.config.AuthConfig;
+import com.sf.oauth.enums.AuthDefaultSource;
+import com.sf.oauth.config.AuthSource;
+import com.sf.oauth.exception.AuthException;
+import com.sf.oauth.domain.AuthCallback;
+
+/**
+ * 授权配置类的校验器
+ *
+ * @author ZK
+ */
+public class AuthChecker {
+
+ /**
+ * 是否支持第三方登录
+ *
+ * @param config config
+ * @param source source
+ * @return true or false
+ */
+ public static boolean isSupportedAuth(AuthConfig config, AuthSource source) {
+
+ return StrUtil.isNotEmpty(config.getClientId())
+ && StrUtil.isNotEmpty(config.getClientSecret())
+ && null != source;
+ }
+
+ /**
+ * 检查配置合法性。针对部分平台, 对redirect uri有特定要求。一般来说redirect uri都是http://,
+ * 而对于部分平台, redirect uri 必须是https的链接
+ *
+ * @param config config
+ * @param source source
+ */
+ public static void checkConfig(AuthConfig config, AuthSource source) {
+ String redirectUri = config.getRedirectUri();
+ if (StrUtil.isEmpty(redirectUri)) {
+ throw new AuthException(HttpStatus.BAD_REQUEST, "Illegal redirect uri", source);
+ }
+ if (!AuthUtils.isHttpProtocol(redirectUri) && !AuthUtils.isHttpsProtocol(redirectUri)) {
+ throw new AuthException(HttpStatus.BAD_REQUEST, "Illegal redirect uri", source);
+ }
+ }
+
+ /**
+ * 校验回调传回的code
+ *
+ * {@code v1.10.0}版本中改为传入{@code source}和{@code callback},对于不同平台使用不同参数接受code的情况统一做处理
+ *
+ * @param source 当前授权平台
+ * @param callback 从第三方授权回调回来时传入的参数集合
+ */
+ public static void checkCode(AuthSource source, AuthCallback callback) {
+ if (StrUtil.isEmpty(callback.getCode())) {
+ throw new AuthException(HttpStatus.UNAUTHORIZED,"Illegal code", source);
+ }
+ }
+
+ /**
+ * 校验回调传回的{@code state},为空或者不存在
+ *
+ * {@code state}不存在的情况只有两种:
+ * 1. {@code state}已使用,被正常清除
+ * 2. {@code state}为前端伪造,本身就不存在
+ *
+ * @param state {@code state}一定不为空
+ * @param source {@code source}当前授权平台
+ */
+ public static void checkState(String state, AuthSource source) {
+ if (StrUtil.isEmpty(state)) {
+ throw new AuthException(HttpStatus.UNAUTHORIZED,"Illegal state", source);
+ }
+ }
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/utils/AuthScopeUtils.java b/sf-oauth/src/main/java/com/sf/oauth/utils/AuthScopeUtils.java
new file mode 100644
index 0000000..9350882
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/utils/AuthScopeUtils.java
@@ -0,0 +1,44 @@
+package com.sf.oauth.utils;
+
+import com.sf.oauth.enums.scope.AuthScope;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Scope 工具类,提供对 scope 类的统一操作
+ *
+ * @author zoukun
+ */
+public class AuthScopeUtils {
+
+ /**
+ * 获取 {@link com.sf.oauth.enums.scope.AuthScope} 数组中所有的被标记为 {@code default} 的 scope
+ *
+ * @param scopes scopes
+ * @return List
+ */
+ public static List getDefaultScopes(AuthScope[] scopes) {
+ if (null == scopes || scopes.length == 0) {
+ return null;
+ }
+ return Arrays.stream(scopes)
+ .filter((AuthScope::isDefault))
+ .map(AuthScope::getScope)
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * 从 {@link com.sf.oauth.enums.scope.AuthScope} 数组中获取实际的 scope 字符串
+ *
+ * @param scopes 可变参数,支持传任意 {@link com.sf.oauth.enums.scope.AuthScope}
+ * @return List
+ */
+ public static List getScopes(AuthScope... scopes) {
+ if (null == scopes || scopes.length == 0) {
+ return null;
+ }
+ return Arrays.stream(scopes).map(AuthScope::getScope).collect(Collectors.toList());
+ }
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/utils/AuthUtils.java b/sf-oauth/src/main/java/com/sf/oauth/utils/AuthUtils.java
new file mode 100644
index 0000000..3d3e22c
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/utils/AuthUtils.java
@@ -0,0 +1,127 @@
+
+package com.sf.oauth.utils;
+
+import cn.hutool.core.util.StrUtil;
+import com.sf.oauth.exception.AuthException;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class AuthUtils {
+ private static final Charset DEFAULT_ENCODING;
+ private static final String HMAC_SHA1 = "HmacSHA1";
+ private static final String HMAC_SHA_256 = "HmacSHA256";
+
+ public AuthUtils() {
+ }
+
+ public static String urlEncode(String value) {
+ if (value == null) {
+ return "";
+ } else {
+ try {
+ String encoded = URLEncoder.encode(value, DEFAULT_ENCODING.displayName());
+ return encoded.replace("+", "%20").replace("*", "%2A").replace("~", "%7E").replace("/", "%2F");
+ } catch (UnsupportedEncodingException var2) {
+ throw new AuthException("Failed To Encode Uri", var2);
+ }
+ }
+ }
+
+ public static String urlDecode(String value) {
+ if (value == null) {
+ return "";
+ } else {
+ try {
+ return URLDecoder.decode(value, DEFAULT_ENCODING.displayName());
+ } catch (UnsupportedEncodingException var2) {
+ throw new AuthException("Failed To Decode Uri", var2);
+ }
+ }
+ }
+
+
+ public static String parseMapToString(Map params, boolean encode) {
+ if (null != params && !params.isEmpty()) {
+ List paramList = new ArrayList();
+ params.forEach((k, v) -> {
+ if (null == v) {
+ paramList.add(k + "=");
+ } else {
+ paramList.add(k + "=" + (encode ? urlEncode(v) : v));
+ }
+
+ });
+ return String.join("&", paramList);
+ } else {
+ return "";
+ }
+ }
+
+ public static boolean isHttpProtocol(String url) {
+ if (StrUtil.isEmpty(url)) {
+ return false;
+ } else {
+ return url.startsWith("http://") || url.startsWith("http%3A%2F%2F");
+ }
+ }
+
+ public static boolean isHttpsProtocol(String url) {
+ if (StrUtil.isEmpty(url)) {
+ return false;
+ } else {
+ return url.startsWith("https://") || url.startsWith("https%3A%2F%2F");
+ }
+ }
+
+ public static boolean isLocalHost(String url) {
+ return StrUtil.isEmpty(url) || url.contains("127.0.0.1") || url.contains("localhost");
+ }
+
+ public static boolean isHttpsProtocolOrLocalHost(String url) {
+ if (StrUtil.isEmpty(url)) {
+ return false;
+ } else {
+ return isHttpsProtocol(url) || isLocalHost(url);
+ }
+ }
+
+
+ public static String getTimestamp() {
+ return String.valueOf(System.currentTimeMillis() / 1000L);
+ }
+
+
+ public static String md5(String str) {
+ MessageDigest md = null;
+ StringBuilder buffer = null;
+
+ try {
+ md = MessageDigest.getInstance("MD5");
+ md.update(str.getBytes(StandardCharsets.UTF_8));
+ byte[] byteData = md.digest();
+ buffer = new StringBuilder();
+ byte[] var4 = byteData;
+ int var5 = byteData.length;
+
+ for(int var6 = 0; var6 < var5; ++var6) {
+ byte byteDatum = var4[var6];
+ buffer.append(Integer.toString((byteDatum & 255) + 256, 16).substring(1));
+ }
+ } catch (Exception var8) {
+ }
+
+ return null == buffer ? "" : buffer.toString();
+ }
+
+ static {
+ DEFAULT_ENCODING = StandardCharsets.UTF_8;
+ }
+}
diff --git a/sf-order/pom.xml b/sf-order/pom.xml
new file mode 100644
index 0000000..d95e016
--- /dev/null
+++ b/sf-order/pom.xml
@@ -0,0 +1,33 @@
+
+
+
+ smarterFramework
+ com.smarterFramework
+ 1.0.0
+
+ 4.0.0
+
+ sf-order
+
+
+ 订单模块
+
+
+
+
+
+
+ com.smarterFramework
+ sf-common
+
+
+ com.smarterFramework
+ sf-framework
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sf-order/src/main/java/com/sf/order/controller/OrderInfoController.java b/sf-order/src/main/java/com/sf/order/controller/OrderInfoController.java
new file mode 100644
index 0000000..37b7d23
--- /dev/null
+++ b/sf-order/src/main/java/com/sf/order/controller/OrderInfoController.java
@@ -0,0 +1,115 @@
+package com.sf.order.controller;
+
+import com.sf.common.annotation.Log;
+import com.sf.common.core.controller.BaseController;
+import com.sf.common.core.domain.AjaxResult;
+import com.sf.common.core.page.TableDataInfo;
+import com.sf.common.enums.BusinessType;
+import com.sf.common.enums.RequestHeaderEnums;
+import com.sf.common.utils.http.RequestUtils;
+import com.sf.common.utils.poi.ExcelUtil;
+import com.sf.order.domain.OrderInfo;
+import com.sf.order.domain.dto.OrderCreateDto;
+import com.sf.order.domain.req.OrderListReqVo;
+import com.sf.order.domain.res.OrderListResVo;
+import com.sf.order.service.IOrderInfoService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletResponse;
+import java.util.List;
+
+/**
+ * 订单基础信息Controller
+ *
+ * @author ztzh
+ * @date 2024-04-09
+ */
+@RestController
+@RequestMapping("/order")
+public class OrderInfoController extends BaseController {
+
+ @Autowired
+ private IOrderInfoService orderInfoService;
+
+ /**
+ * 查询订单基础信息列表
+ */
+ @GetMapping("/list")
+ public TableDataInfo list(OrderListReqVo vo) {
+ vo.setAppCode(RequestUtils.getHeader(RequestHeaderEnums.APP_CODE.getCode()));
+ startPage();
+ List list = orderInfoService.queryList(vo);
+ return getDataTable(list);
+ }
+
+ /**
+ * 我的订单列表(当前用户)
+ */
+ @GetMapping("/myOrderList")
+ public TableDataInfo myOrderList(OrderListReqVo vo) {
+ vo.setAppCode(RequestUtils.getHeader(RequestHeaderEnums.APP_CODE.getCode()));
+ vo.setUserId(getUserId());
+ // startPage();
+ List list = orderInfoService.queryList(vo);
+ return getDataTable(list);
+ }
+
+
+
+ /**
+ * 导出订单基础信息列表
+ */
+ @Log(title = "订单基础信息", businessType = BusinessType.EXPORT)
+ @PostMapping("/export")
+ public void export(HttpServletResponse response, OrderInfo orderInfo) {
+ List list = orderInfoService.selectOrderInfoList(orderInfo);
+ ExcelUtil util = new ExcelUtil(OrderInfo.class);
+ util.exportExcel(response, list, "订单基础信息数据");
+ }
+
+ /**
+ * 获取订单基础信息详细信息
+ */
+ @GetMapping(value = "/{id}")
+ public AjaxResult getInfo(@PathVariable("id") Long id) {
+ return success(orderInfoService.selectOrderInfoById(id));
+ }
+
+ /**
+ * 新增订单基础信息
+ */
+ @Log(title = "创建订单基础信息", businessType = BusinessType.INSERT)
+ @PostMapping(value = "/createOrder")
+ public AjaxResult createOrder(@RequestBody OrderCreateDto orderCreateDto) {
+ orderCreateDto.setAppCode(RequestUtils.getHeader(RequestHeaderEnums.APP_CODE.getCode()));
+ return AjaxResult.success(orderInfoService.createOrder(orderCreateDto));
+ }
+
+ /**
+ * 修改订单基础信息
+ */
+ @Log(title = "订单基础信息", businessType = BusinessType.UPDATE)
+ @PutMapping
+ public AjaxResult edit(@RequestBody OrderInfo orderInfo) {
+ return toAjax(orderInfoService.updateOrderInfo(orderInfo));
+ }
+
+ /**
+ * 支付订单
+ */
+ @PostMapping(value = "/pay/{orderId}")
+ private String orderPay(@PathVariable(value = "orderId") Long orderId) {
+ orderInfoService.orderPay(orderId);
+ return null;
+ }
+
+ /**
+ * 删除订单基础信息
+ */
+ @Log(title = "订单基础信息", businessType = BusinessType.DELETE)
+ @DeleteMapping("/{ids}")
+ public AjaxResult remove(@PathVariable Long[] ids) {
+ return toAjax(orderInfoService.deleteOrderInfoByIds(ids));
+ }
+}
diff --git a/sf-order/src/main/java/com/sf/order/domain/OrderInfo.java b/sf-order/src/main/java/com/sf/order/domain/OrderInfo.java
new file mode 100644
index 0000000..ca65473
--- /dev/null
+++ b/sf-order/src/main/java/com/sf/order/domain/OrderInfo.java
@@ -0,0 +1,187 @@
+package com.sf.order.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.sf.common.annotation.Excel;
+import com.sf.common.core.domain.BaseEntity;
+import lombok.Data;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import java.util.Date;
+
+/**
+ * 订单基础信息对象 ORDER_INFO
+ *
+ * @author ztzh
+ * @date 2024-04-09
+ */
+@Data
+public class OrderInfo extends BaseEntity {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 主键
+ */
+ private Long id;
+
+ /**
+ * 订单编号
+ */
+ @Excel(name = "订单编号")
+ private Long orderNo;
+
+ /**
+ * 订单状态: 订单状态: 0:待支付 1:已付款 2:支付超时系统结束 3:客户自主取消 4:已完成
+ */
+ @Excel(name = "订单状态: 0:待支付 1:已付款 2:支付超时系统结束 3:客户自主取消 4:已完成")
+ private Long orderStatus;
+
+ /**
+ * 支付方式:0-点券 1-现金
+ */
+ @Excel(name = "支付方式:0-点券 1-现金")
+ private Long payType;
+
+ /**
+ * 支付渠道(支付方式为现金时)
+ * 0:微信
+ * 1:支付宝
+ */
+ @Excel(name = "支付渠道(支付方式为现金时) 0:微信 1:支付宝")
+ private Long payChannel;
+
+ /**
+ * 订单金额
+ */
+ @Excel(name = "订单金额")
+ private Long orderAmt;
+
+ /**
+ * 运费
+ */
+ @Excel(name = "运费")
+ private Long freightAmt;
+
+ /**
+ * 总金额
+ */
+ @Excel(name = "总金额")
+ private Long payAmt;
+
+ /**
+ * 实际支付金额
+ */
+ @Excel(name = "实际支付金额")
+ private Long reallyAmt;
+
+ /**
+ * 收件方式
+ * :0-自提
+ * 1-快递 2-配送
+ */
+ @Excel(name = "收件方式:0-自提 1-快递 2-配送")
+ private Long receiveType;
+
+ /**
+ * 商品Id
+ */
+ @Excel(name = "商品Id")
+ private Long goodsId;
+
+ /**
+ * 商户Id
+ */
+ @Excel(name = "商户Id")
+ private Long businessId;
+
+ /**
+ * 收货地址配置Id
+ */
+ @Excel(name = "收货地址配置Id")
+ private Long receiveAddrId;
+
+ /**
+ * 支付时间
+ */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @Excel(name = "支付时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+ private Date payTime;
+
+ /**
+ * 订单创建人
+ */
+ @Excel(name = "订单创建人")
+ private Long createUserId;
+
+ /**
+ * 最后一次更新操作人
+ */
+ @Excel(name = "最后一次更新操作人")
+ private Long updateUserId;
+
+ /**
+ * 逻辑删除标识
+ * 0:未删除
+ * 1:已删除
+ */
+ @Excel(name = "逻辑删除标识 0:未删除 1:已删除")
+ private Long isDelete;
+
+ /**
+ * 快递单号
+ */
+ @Excel(name = "快递单号")
+ private String trackNo;
+
+ /**
+ * 订单类型0:自动1:手动
+ */
+ @Excel(name = "订单类型0:自动1:手动")
+ private Long orderType;
+
+ /**
+ * 平台外部订单号
+ */
+ @Excel(name = "平台外部订单号")
+ private String outOrderNo;
+
+ /**
+ * 平台支付返回值
+ */
+ @Excel(name = "平台支付返回值")
+ private String payData;
+
+ /**
+ * 减免金额(优惠券抵扣)
+ */
+ @Excel(name = "减免金额(优惠券抵扣)")
+ private Long reductionAmout;
+
+ /**
+ * * 商品数量
+ */
+ private Integer goodsCount;
+ /**
+ * * 商品类型。
+ * * • 0:消耗型商品
+ * * • 1:非消耗型商品
+ * * • 2:自动续期订阅商品
+ */
+ private Integer goodsType;
+
+ /**
+ * 商品单价
+ */
+ private Long goodsPrice;
+
+ /**
+ * 商品编号
+ */
+ private String goodsCode;
+ /**
+ * appCode
+ */
+ private String appCode;
+
+
+}
diff --git a/sf-order/src/main/java/com/sf/order/domain/dto/OrderCreateDto.java b/sf-order/src/main/java/com/sf/order/domain/dto/OrderCreateDto.java
new file mode 100644
index 0000000..3107020
--- /dev/null
+++ b/sf-order/src/main/java/com/sf/order/domain/dto/OrderCreateDto.java
@@ -0,0 +1,80 @@
+package com.sf.order.domain.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * @author : ztzh
+ * @since : 2023/7/27
+ */
+@Schema(description = "下单")
+public class OrderCreateDto {
+
+ @Schema(description = "商品id")
+ @NotNull(message = "商品id不能为空")
+ private Long goodsId;
+
+ @Schema(description = "平台外部订单号")
+ private String outOrderNo;
+
+ @Schema(description = "数量")
+ private Long count;
+
+ @Schema(description = "金额")
+ private Long amount;
+
+ @Schema(description = "用户id")
+ @NotNull(message = "用户id不能空")
+ private Long userId;
+ @Schema(description = "应用code")
+ @NotNull(message = "应用code不能空")
+ private String appCode;
+
+ public Long getGoodsId() {
+ return goodsId;
+ }
+
+ public void setGoodsId(Long goodsId) {
+ this.goodsId = goodsId;
+ }
+ public Long getCount() {
+ return count;
+ }
+
+ public void setCount(Long count) {
+ this.count = count;
+ }
+
+ public Long getAmount() {
+ return amount;
+ }
+
+ public void setAmount(Long amount) {
+ this.amount = amount;
+ }
+ public Long getUserId() {
+ return userId;
+ }
+
+ public void setUserId(Long userId) {
+ this.userId = userId;
+ }
+
+ public String getOutOrderNo() {
+ return outOrderNo;
+ }
+
+ public void setOutOrderNo(String outOrderNo) {
+ this.outOrderNo = outOrderNo;
+ }
+
+ public String getAppCode() {
+ return appCode;
+ }
+
+ public void setAppCode(String appCode) {
+ this.appCode = appCode;
+ }
+
+}
diff --git a/sf-order/src/main/java/com/sf/order/domain/req/OrderListReqVo.java b/sf-order/src/main/java/com/sf/order/domain/req/OrderListReqVo.java
new file mode 100644
index 0000000..b640967
--- /dev/null
+++ b/sf-order/src/main/java/com/sf/order/domain/req/OrderListReqVo.java
@@ -0,0 +1,27 @@
+package com.sf.order.domain.req;
+
+import com.sf.common.core.domain.BaseEntity;
+import lombok.Data;
+
+@Data
+public class OrderListReqVo extends BaseEntity {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 主键id
+ */
+ private Long userId;
+
+ /**
+ * 订单状态
+ */
+ private Long orderStatus;
+
+ private Integer pageSize;
+
+ private Integer pageNo;
+
+ private String appCode;
+
+}
diff --git a/sf-order/src/main/java/com/sf/order/domain/res/OrderListResVo.java b/sf-order/src/main/java/com/sf/order/domain/res/OrderListResVo.java
new file mode 100644
index 0000000..85533be
--- /dev/null
+++ b/sf-order/src/main/java/com/sf/order/domain/res/OrderListResVo.java
@@ -0,0 +1,97 @@
+package com.sf.order.domain.res;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.sf.common.annotation.Excel;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 活动信息详情
+ *
+ */
+@Data
+@Schema(name = "OrderListResVo", description = "订单列表")
+public class OrderListResVo {
+ /**
+ * 主键id
+ */
+ private Long id;
+
+ /**
+ * 订单编号
+ */
+ private String orderNo;
+
+ /**
+ * 订单状态: 0:待支付 1:已付款待发货 2:配送中 3:待取货 4:支付超时系统结束 5:客户自主取消 6:已完成
+ */
+ private Long orderStatus;
+
+ /**
+ * 订单金额
+ */
+ private Long orderAmt;
+
+ /**
+ * 支付时间
+ */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private Date payTime;
+
+ /**
+ * 订阅订单取消时间
+ */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private Date subscriptionCancellationTime;
+
+ /**
+ * 商品标题
+ */
+ private String productTitle;
+ /**
+ * 图片
+ */
+ private String productPicture;
+
+ /**
+ * 商品描述
+ */
+ private String productDesc;
+
+
+ /**
+ * 商品规格
+ */
+ private String goodsSpec;
+
+ /**
+ * 商品数量
+ */
+ private Integer goodsCount;
+
+ /**
+ * 商品价格
+ */
+ private Long goodsPrice;
+
+ /**
+ * * 商品类型。
+ * * • 0:消耗型商品
+ * * • 1:非消耗型商品
+ * * • 2:自动续期订阅商品
+ */
+ private Integer goodsType;
+
+ /**
+ * 商品编码
+ */
+ private String goodsCode;
+ /**
+ * 应用编码
+ */
+ private String appCode;
+
+
+}
diff --git a/sf-order/src/main/java/com/sf/order/mapper/OrderInfoMapper.java b/sf-order/src/main/java/com/sf/order/mapper/OrderInfoMapper.java
new file mode 100644
index 0000000..342981a
--- /dev/null
+++ b/sf-order/src/main/java/com/sf/order/mapper/OrderInfoMapper.java
@@ -0,0 +1,69 @@
+package com.sf.order.mapper;
+
+import com.sf.order.domain.OrderInfo;
+import com.sf.order.domain.req.OrderListReqVo;
+import com.sf.order.domain.res.OrderListResVo;
+
+import java.util.List;
+
+
+/**
+ * 订单基础信息Mapper接口
+ *
+ * @author ztzh
+ * @date 2024-04-09
+ */
+public interface OrderInfoMapper {
+ /**
+ * 查询订单基础信息
+ *
+ * @param id 订单基础信息主键
+ * @return 订单基础信息
+ */
+ public OrderInfo selectOrderInfoById(Long id);
+
+ /**
+ * 查询订单基础信息列表
+ *
+ * @param orderInfo 订单基础信息
+ * @return 订单基础信息集合
+ */
+ public List selectOrderInfoList(OrderInfo orderInfo);
+
+
+ List queryList(OrderListReqVo vo);
+
+ /**
+ * 新增订单基础信息
+ *
+ * @param orderInfo 订单基础信息
+ * @return 结果
+ */
+ public int insertOrderInfo(OrderInfo orderInfo);
+
+ /**
+ * 修改订单基础信息
+ *
+ * @param orderInfo 订单基础信息
+ * @return 结果
+ */
+ public int updateOrderInfo(OrderInfo orderInfo);
+
+ /**
+ * 删除订单基础信息
+ *
+ * @param id 订单基础信息主键
+ * @return 结果
+ */
+ public int deleteOrderInfoById(Long id);
+
+ /**
+ * 批量删除订单基础信息
+ *
+ * @param ids 需要删除的数据主键集合
+ * @return 结果
+ */
+ public int deleteOrderInfoByIds(Long[] ids);
+
+ OrderInfo selectOrderInfoByOrderNo(String orderNo);
+}
diff --git a/sf-order/src/main/java/com/sf/order/service/IOrderInfoService.java b/sf-order/src/main/java/com/sf/order/service/IOrderInfoService.java
new file mode 100644
index 0000000..9904bb6
--- /dev/null
+++ b/sf-order/src/main/java/com/sf/order/service/IOrderInfoService.java
@@ -0,0 +1,78 @@
+package com.sf.order.service;
+
+
+import com.sf.order.domain.OrderInfo;
+import com.sf.order.domain.dto.OrderCreateDto;
+import com.sf.order.domain.req.OrderListReqVo;
+import com.sf.order.domain.res.OrderListResVo;
+
+import java.util.List;
+
+/**
+ * 订单基础信息Service接口
+ *
+ * @author ztzh
+ * @date 2024-04-09
+ */
+public interface IOrderInfoService
+{
+ /**
+ * 查询订单基础信息
+ *
+ * @param id 订单基础信息主键
+ * @return 订单基础信息
+ */
+ public OrderInfo selectOrderInfoById(Long id);
+
+ /**
+ * 查询订单基础信息列表
+ *
+ * @param orderInfo 订单基础信息
+ * @return 订单基础信息集合
+ */
+ public List selectOrderInfoList(OrderInfo orderInfo);
+
+ /**
+ * 查询订单基础信息列表
+ */
+ List queryList(OrderListReqVo vo);
+
+ /**
+ * 新增订单基础信息
+ *
+ * @param orderInfo 订单基础信息
+ * @return 结果
+ */
+ public Long createOrder(OrderCreateDto orderInfo);
+
+ /**
+ * 修改订单基础信息
+ *
+ * @param orderInfo 订单基础信息
+ * @return 结果
+ */
+ public int updateOrderInfo(OrderInfo orderInfo);
+
+ /**
+ * 批量删除订单基础信息
+ *
+ * @param ids 需要删除的订单基础信息主键集合
+ * @return 结果
+ */
+ public int deleteOrderInfoByIds(Long[] ids);
+
+ /**
+ * 删除订单基础信息信息
+ *
+ * @param id 订单基础信息主键
+ * @return 结果
+ */
+ public int deleteOrderInfoById(Long id);
+
+ void orderPay(Long orderId);
+
+ OrderInfo selectOrderInfoByOrderNo(String orderNo);
+
+ void insertOrder(OrderInfo orderInfo);
+
+}
diff --git a/sf-order/src/main/java/com/sf/order/service/impl/OrderInfoServiceImpl.java b/sf-order/src/main/java/com/sf/order/service/impl/OrderInfoServiceImpl.java
new file mode 100644
index 0000000..655d09e
--- /dev/null
+++ b/sf-order/src/main/java/com/sf/order/service/impl/OrderInfoServiceImpl.java
@@ -0,0 +1,145 @@
+package com.sf.order.service.impl;
+
+import com.sf.common.exception.ServiceException;
+import com.sf.common.utils.DateUtils;
+import com.sf.common.utils.SnowflakeIdWorker;
+import com.sf.order.domain.OrderInfo;
+import com.sf.order.domain.dto.OrderCreateDto;
+import com.sf.order.domain.req.OrderListReqVo;
+import com.sf.order.domain.res.OrderListResVo;
+import com.sf.order.mapper.OrderInfoMapper;
+import com.sf.order.service.IOrderInfoService;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+/**
+ * 订单基础信息Service业务层处理
+ *
+ * @author ztzh
+ * @date 2024-04-09
+ */
+@Service
+public class OrderInfoServiceImpl implements IOrderInfoService {
+ @Resource
+ private OrderInfoMapper orderInfoMapper;
+ @Resource
+ private SnowflakeIdWorker snowflakeIdWorker;
+
+ /**
+ * 查询订单基础信息
+ *
+ * @param id 订单基础信息主键
+ * @return 订单基础信息
+ */
+ @Override
+ public OrderInfo selectOrderInfoById(Long id) {
+ return orderInfoMapper.selectOrderInfoById(id);
+ }
+
+ /**
+ * 查询订单基础信息列表
+ *
+ * @param orderInfo 订单基础信息
+ * @return 订单基础信息
+ */
+ @Override
+ public List selectOrderInfoList(OrderInfo orderInfo) {
+ return orderInfoMapper.selectOrderInfoList(orderInfo);
+ }
+
+ public List queryList(OrderListReqVo vo) {
+ return orderInfoMapper.queryList(vo);
+ }
+
+ /**
+ * 新增订单基础信息
+ *
+ * @param orderCreateDto 订单基础信息
+ * @return 结果
+ */
+ @Override
+ public Long createOrder(OrderCreateDto orderCreateDto) {
+ OrderInfo orderInfo = new OrderInfo();
+ long id = snowflakeIdWorker.nextId();
+ orderInfo.setOrderNo(id);
+ orderInfo.setPayType(1L);
+ orderInfo.setReceiveType(0L);
+ orderInfo.setOrderStatus(0L);
+ orderInfo.setCreateUserId(orderCreateDto.getUserId());
+ orderInfo.setUpdateUserId(orderCreateDto.getUserId());
+ orderInfo.setGoodsId(orderCreateDto.getGoodsId());
+ orderInfo.setOrderAmt(orderCreateDto.getAmount());
+ orderInfo.setPayAmt(orderCreateDto.getAmount());
+ orderInfo.setCreateTime(DateUtils.getNowDate());
+ orderInfo.setUpdateTime(DateUtils.getNowDate());
+ orderInfo.setOutOrderNo(orderCreateDto.getOutOrderNo());
+ orderInfo.setAppCode(orderCreateDto.getAppCode());
+ orderInfoMapper.insertOrderInfo(orderInfo);
+ return id;
+ }
+
+ /**
+ * 修改订单基础信息
+ *
+ * @param orderInfo 订单基础信息
+ * @return 结果
+ */
+ @Override
+ public int updateOrderInfo(OrderInfo orderInfo) {
+ orderInfo.setUpdateTime(DateUtils.getNowDate());
+ return orderInfoMapper.updateOrderInfo(orderInfo);
+ }
+
+ @Override
+ public void orderPay(Long orderId) {
+ OrderInfo updateOrder = this.selectOrderInfoById(orderId);
+ if (updateOrder == null) {
+ throw new ServiceException("订单缺失!");
+ }
+// if (!OrderInfoConstant.ORDER_WAITING_FOR_PAYMENT.equals(orderInfo.getOrderStatus())) {
+// throw new ServiceException("订单状态异常!");
+// }
+ updateOrder.setOrderStatus(1L);
+ updateOrder.setPayTime(DateUtils.getNowDate());
+ // 修改订单状态
+ if (1 > this.updateOrderInfo(updateOrder)) {
+ throw new ServiceException("支付异常,请联系管理员!");
+ }
+ }
+
+ @Override
+ public OrderInfo selectOrderInfoByOrderNo(String orderNo) {
+
+ return orderInfoMapper.selectOrderInfoByOrderNo(orderNo);
+ }
+
+ @Override
+ public void insertOrder(OrderInfo orderInfo) {
+ orderInfoMapper.insertOrderInfo(orderInfo);
+ }
+
+ /**
+ * 批量删除订单基础信息
+ *
+ * @param ids 需要删除的订单基础信息主键
+ * @return 结果
+ */
+ @Override
+ public int deleteOrderInfoByIds(Long[] ids) {
+ return orderInfoMapper.deleteOrderInfoByIds(ids);
+ }
+
+ /**
+ * 删除订单基础信息信息
+ *
+ * @param id 订单基础信息主键
+ * @return 结果
+ */
+ @Override
+ public int deleteOrderInfoById(Long id) {
+ return orderInfoMapper.deleteOrderInfoById(id);
+ }
+
+}
diff --git a/sf-order/src/main/resources/mapper/order/OrderInfoMapper.xml b/sf-order/src/main/resources/mapper/order/OrderInfoMapper.xml
new file mode 100644
index 0000000..3a208c6
--- /dev/null
+++ b/sf-order/src/main/resources/mapper/order/OrderInfoMapper.xml
@@ -0,0 +1,211 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ select id, order_no, order_status, pay_type, pay_channel, order_amt, freight_amt, pay_amt, really_amt, receive_type, goods_id, business_id, receive_addr_id, create_time, pay_time, create_user_id, update_user_id, is_delete, update_time, track_no, order_type, out_order_no, pay_data, reduction_amout from Order_info
+
+
+ SELECT a.id,a.app_code,a.order_no,a.order_status,a.order_amt,a.pay_time,a.goods_count,a.subscription_cancellation_time,a.goods_type,a.goods_price,a.goods_code,b.product_title,b.product_picture,b.product_desc,b.goods_spec
+ FROM Order_info a LEFT JOIN GOODS_MESSAGES b ON a.goods_id = b.id
+
+
+
+
+
+
+
+
+
+
+ insert into Order_info
+
+ id,
+ order_no,
+ order_status,
+ pay_type,
+ pay_channel,
+ order_amt,
+ freight_amt,
+ pay_amt,
+ really_amt,
+ receive_type,
+ goods_id,
+ business_id,
+ receive_addr_id,
+ create_time,
+ pay_time,
+ create_user_id,
+ update_user_id,
+ is_delete,
+ update_time,
+ track_no,
+ order_type,
+ out_order_no,
+ pay_data,
+ reduction_amout,
+ goods_count,
+ goods_type,
+ goods_price,
+ goods_code,
+ app_code,
+
+
+ #{id},
+ #{orderNo},
+ #{orderStatus},
+ #{payType},
+ #{payChannel},
+ #{orderAmt},
+ #{freightAmt},
+ #{payAmt},
+ #{reallyAmt},
+ #{receiveType},
+ #{goodsId},
+ #{businessId},
+ #{receiveAddrId},
+ #{createTime},
+ #{payTime},
+ #{createUserId},
+ #{updateUserId},
+ #{isDelete},
+ #{updateTime},
+ #{trackNo},
+ #{orderType},
+ #{outOrderNo},
+ #{payData},
+ #{reductionAmout},
+ #{goodsCount},
+ #{goodsType},
+ #{goodsPrice},
+ #{goodsCode},
+ #{appCode},
+
+
+
+
+ update Order_info
+
+ order_no = #{orderNo},
+ order_status = #{orderStatus},
+ pay_type = #{payType},
+ pay_channel = #{payChannel},
+ order_amt = #{orderAmt},
+ freight_amt = #{freightAmt},
+ pay_amt = #{payAmt},
+ really_amt = #{reallyAmt},
+ receive_type = #{receiveType},
+ goods_id = #{goodsId},
+ business_id = #{businessId},
+ receive_addr_id = #{receiveAddrId},
+ create_time = #{createTime},
+ pay_time = #{payTime},
+ create_user_id = #{createUserId},
+ update_user_id = #{updateUserId},
+ is_delete = #{isDelete},
+ update_time = #{updateTime},
+ track_no = #{trackNo},
+ order_type = #{orderType},
+ out_order_no = #{outOrderNo},
+ pay_data = #{payData},
+ reduction_amout = #{reductionAmout},
+
+ where id = #{id}
+
+
+
+ delete from Order_info where id = #{id}
+
+
+
+ delete from Order_info where id in
+
+ #{id}
+
+
+
\ No newline at end of file
diff --git a/sf-payment/pom.xml b/sf-payment/pom.xml
new file mode 100644
index 0000000..680816a
--- /dev/null
+++ b/sf-payment/pom.xml
@@ -0,0 +1,50 @@
+
+
+
+ smarterFramework
+ com.smarterFramework
+ 1.0.0
+
+ 4.0.0
+
+ sf-payment
+
+
+ 支付模块
+
+
+
+
+
+
+ com.smarterFramework
+ sf-common
+
+
+ com.smarterFramework
+ sf-framework
+
+
+ com.smarterFramework
+ sf-order
+
+
+ com.smarterFramework
+ sf-service
+
+
+ org.bouncycastle
+ bcprov-jdk18on
+ 1.73
+
+
+ com.auth0
+ java-jwt
+ 4.4.0
+
+
+
+
+
\ No newline at end of file
diff --git a/sf-payment/src/main/java/com/sf/payment/config/HuaweiPaymentConfig.java b/sf-payment/src/main/java/com/sf/payment/config/HuaweiPaymentConfig.java
new file mode 100644
index 0000000..f9d76f5
--- /dev/null
+++ b/sf-payment/src/main/java/com/sf/payment/config/HuaweiPaymentConfig.java
@@ -0,0 +1,37 @@
+package com.sf.payment.config;
+
+import lombok.Data;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+
+
+/**
+ * 功能描述:
+ *
+ * @author a_kun
+ * @date 2024/4/17 9:23
+ */
+@Data
+@Configuration
+public class HuaweiPaymentConfig {
+
+ /**
+ * 客户端id:(oauth用)
+ */
+ @Value("${huawei.payment.clientId:110693217}")
+ private String clientId;
+
+ /**
+ * 客户端Secret:对应各平台的appSecret
+ */
+ @Value("${huawei.payment.clientSecret:1410c01bc71c7ba587175ae79e500137c70945acc1416a38127cf98a09a6f8ba}")
+ private String clientSecret;
+
+ /**
+ * 应用id
+ */
+ @Value("${huawei.payment.appId:5765880207854169373}")
+ private String appId;
+
+
+}
diff --git a/sf-payment/src/main/java/com/sf/payment/constant/GoodsConstants.java b/sf-payment/src/main/java/com/sf/payment/constant/GoodsConstants.java
new file mode 100644
index 0000000..53b8b1c
--- /dev/null
+++ b/sf-payment/src/main/java/com/sf/payment/constant/GoodsConstants.java
@@ -0,0 +1,29 @@
+package com.sf.payment.constant;
+
+import io.jsonwebtoken.Claims;
+
+/**
+ * 商品信息
+ *
+ * @author zoukun
+ */
+public class GoodsConstants {
+ /**
+ * 商品类型。
+ * • 0:消耗型商品
+ */
+ public static final Integer GOODS_TYPE_CONSUMABLE = 0;
+
+ /**
+ * 商品类型。
+ * • 1:非消耗型商品
+ */
+ public static final Integer GOODS_TYPE_NON_CONSUMABLE = 1;
+
+ /**
+ * 商品类型。
+ * • 2:自动续期订阅商品
+ */
+ public static final Integer GOODS_TYPE_AUTOMATIC_RENEWAL_SUBSCRIPTION = 2;
+
+}
diff --git a/sf-payment/src/main/java/com/sf/payment/controller/HuaweiPaymentController.java b/sf-payment/src/main/java/com/sf/payment/controller/HuaweiPaymentController.java
new file mode 100644
index 0000000..34eafee
--- /dev/null
+++ b/sf-payment/src/main/java/com/sf/payment/controller/HuaweiPaymentController.java
@@ -0,0 +1,48 @@
+package com.sf.payment.controller;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.sf.common.core.domain.AjaxResult;
+import com.sf.payment.constant.GoodsConstants;
+import com.sf.payment.domain.HuaweiPaymentCallback;
+import com.sf.payment.domain.HuaweiPurchasesVerifyDTO;
+import com.sf.payment.service.IHuaweiPaymentService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.util.Assert;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * @author zoukun
+ */
+@Slf4j
+@RestController
+@RequestMapping("/payment")
+public class HuaweiPaymentController {
+
+ @Autowired
+ private IHuaweiPaymentService huaweiPaymentService;
+
+
+ /**
+ * 华为支付回调地址
+ */
+ @RequestMapping("/callback/huawei")
+ public AjaxResult callback(@RequestBody HuaweiPaymentCallback callback) {
+ log.info("进入callback: params:" + JSONObject.toJSONString(callback));
+ AjaxResult ajax = AjaxResult.success();
+ return ajax;
+ }
+
+
+ /**
+ * 华为购买验证(订购会员)
+ */
+ @PostMapping("/huawei/purchases/verify")
+ public AjaxResult purchasesVerify(@Validated @RequestBody HuaweiPurchasesVerifyDTO verifyDTO) {
+ log.info("进入/huawei/purchases/tokens/verify: params:" + JSONObject.toJSONString(verifyDTO));
+ huaweiPaymentService.purchasesVerify(verifyDTO);
+ return AjaxResult.success();
+ }
+
+}
diff --git a/sf-payment/src/main/java/com/sf/payment/domain/HuaweiPaymentCallback.java b/sf-payment/src/main/java/com/sf/payment/domain/HuaweiPaymentCallback.java
new file mode 100644
index 0000000..b7e46d2
--- /dev/null
+++ b/sf-payment/src/main/java/com/sf/payment/domain/HuaweiPaymentCallback.java
@@ -0,0 +1,35 @@
+package com.sf.payment.domain;
+
+import lombok.*;
+
+import java.io.Serializable;
+
+/**
+ * 授权回调时的参数类
+ *
+ * @author zk
+ */
+@Getter
+@Setter
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class HuaweiPaymentCallback implements Serializable {
+
+ /**
+ * 访问AuthorizeUrl后回调时带的参数code
+ */
+ private String code;
+
+ /**
+ * 客户端id:对应各平台的appKey
+ */
+ private String clientId;
+
+ /**
+ * 客户端Secret:对应各平台的appSecret
+ */
+ private String clientSecret;
+
+
+}
diff --git a/sf-payment/src/main/java/com/sf/payment/domain/HuaweiPurchasesVerifyDTO.java b/sf-payment/src/main/java/com/sf/payment/domain/HuaweiPurchasesVerifyDTO.java
new file mode 100644
index 0000000..49a250c
--- /dev/null
+++ b/sf-payment/src/main/java/com/sf/payment/domain/HuaweiPurchasesVerifyDTO.java
@@ -0,0 +1,53 @@
+package com.sf.payment.domain;
+
+import apijson.NotNull;
+import lombok.*;
+
+import javax.validation.constraints.NotBlank;
+import java.io.Serializable;
+
+/**
+ * 华为验证购买tokenDTO
+ *
+ * @author zk
+ */
+@Getter
+@Setter
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class HuaweiPurchasesVerifyDTO implements Serializable {
+
+
+
+ /**
+ * 商品类型。
+ * • 0:消耗型商品
+ * • 1:非消耗型商品
+ * • 2:自动续期订阅商品
+ */
+ @NotNull
+ private Integer type;
+
+ /**
+ * 包含订单信息的JWS格式数据。
+ * 可参见对返回结果验签
+ * 解码验签获取相关购买数据的JSON字符串,
+ * 其包含的参数请参见PurchaseOrderPayload。
+ */
+ private String jwsPurchaseOrder;
+
+ /**
+ * 包含订阅状态信息的
+ * JWS格式数据。
+ * 可参见对返回结果验签
+ * 解码验签获取相关订阅状态
+ * 信息的JSON字符串,
+ * 其包含的参数请参见
+ * SubGroupStatusPayload。
+ */
+ private String jwsSubscriptionStatus;
+
+
+
+}
diff --git a/sf-payment/src/main/java/com/sf/payment/domain/HuaweiPurchasesVerifyResponseDTO.java b/sf-payment/src/main/java/com/sf/payment/domain/HuaweiPurchasesVerifyResponseDTO.java
new file mode 100644
index 0000000..c684a59
--- /dev/null
+++ b/sf-payment/src/main/java/com/sf/payment/domain/HuaweiPurchasesVerifyResponseDTO.java
@@ -0,0 +1,56 @@
+package com.sf.payment.domain;
+
+import apijson.NotNull;
+import lombok.*;
+
+import javax.validation.constraints.NotBlank;
+import java.io.Serializable;
+
+/**
+ * 华为验证购买tokenD返回
+ *
+ * @author zk
+ */
+@Getter
+@Setter
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class HuaweiPurchasesVerifyResponseDTO implements Serializable {
+
+ /**
+ * 返回码。
+ *
+ * 0:成功。
+ * 其他:失败,具体请参见错误码。
+ */
+ private String responseCode;
+
+ /**
+ * 响应描述。
+ */
+ private String responseMessage;
+
+ /**
+ *
+ * 包含购买数据的JSON字符串,具体请参见表InappPurchaseDetails。
+ *
+ * 该字段原样参与签名。
+ */
+ private String purchaseTokenData;
+
+ /**
+ * purchaseTokenData基于应用RSA IAP私钥的签名信息,签名算法为signatureAlgorithm。
+ * 应用请参见对返回结果验签使用IAP公钥对PurchaseTokenData的JSON字符串进行验签。
+ */
+ private String dataSignature;
+
+ /**
+ *
+ * 签名算法。
+ */
+ @NotBlank(message = "待下发商品ID不能为空")
+ private String signatureAlgorithm;
+
+
+}
diff --git a/sf-payment/src/main/java/com/sf/payment/domain/HuaweiQueryResponse.java b/sf-payment/src/main/java/com/sf/payment/domain/HuaweiQueryResponse.java
new file mode 100644
index 0000000..4814e9f
--- /dev/null
+++ b/sf-payment/src/main/java/com/sf/payment/domain/HuaweiQueryResponse.java
@@ -0,0 +1,35 @@
+package com.sf.payment.domain;
+
+import lombok.Data;
+
+/**
+ * 功能描述:
+ *
+ * @author a_kun
+ * @date 2024/4/17 14:41
+ */
+@Data
+public class HuaweiQueryResponse {
+
+ /**
+ * 返回码。
+ * • 0:成功。
+ * • 其他:失败,具体请参见错误码
+ */
+ private String responseCode;
+
+ /**
+ * 响应描述。
+ */
+ private String responseMessage;
+
+ /**
+ * 包含已购订单相关状态信息的JWS格式数据。可参见对返回结果验签解码验签获取相关订单状态信息的JSON字符串,其包含的参数具体请参见表PurchaseOrderPayload说明。
+ */
+ private String jwsPurchaseOrder;
+
+ /**
+ * 包含已购订阅相关状态信息的JWS格式数据。可参见对返回结果验签解码验签获取相关订阅状态信息的JSON字符串,其包含的参数请参见SubGroupStatusPayload
+ */
+ private String jwsSubGroupStatus;
+}
diff --git a/sf-payment/src/main/java/com/sf/payment/domain/InAppPurchaseData.java b/sf-payment/src/main/java/com/sf/payment/domain/InAppPurchaseData.java
new file mode 100644
index 0000000..05f7e97
--- /dev/null
+++ b/sf-payment/src/main/java/com/sf/payment/domain/InAppPurchaseData.java
@@ -0,0 +1,233 @@
+package com.sf.payment.domain;
+
+import lombok.Data;
+
+/**
+ * 功能描述:
+ *
+ * @author a_kun
+ * @date 2024/4/16 15:29
+ */
+@Data
+public class InAppPurchaseData {
+
+ /**
+ * 应用ID。
+ */
+ private Long applicationId;
+
+ /**
+ * 消耗型商品或者非消耗型商品:固定为false。
+ *
+ * 订阅型商品:
+ *
+ * true:订阅处于活动状态并且将在下一个结算日期自动续订。
+ * false:用户已经取消订阅。 用户可以在下一个结算日期之前访问订阅内容,并且在该日期后将无法访问,除非重新启用自动续订。 如果提供了宽限期,只要宽限期未过,此值就会对所有订阅保持设置为true。 下一次结算日期每天都会自动延长,直至宽限期结束,或者用户更改付款方式。
+ */
+ private Boolean autoRenewing;
+
+ /**
+ * 订单ID,唯一标识一笔需要收费的收据,由华为应用内支付服务器在创建订单以及订阅型商品续费时生成。
+ *
+ * 每一笔新的收据都会使用不同的orderId。
+ */
+ private String orderId;
+
+ /**
+ * 商品类别,取值包括:
+ *
+ * 0:消耗型商品
+ * 1:非消耗型商品
+ * 2:订阅型商品
+ */
+ private Integer kind;
+
+ /**
+ * 商品ID。每种商品必须有唯一的ID,由应用在PMS中维护,或者应用发起购买时传入。
+ * 说明
+ * 为避免资金损失,您在对支付结果验签成功后,必须对其进行校验。
+ */
+ private String productId;
+
+ /**
+ * 商品名称。
+ */
+ private String productName;
+
+ /**
+ * 商品购买时间,UTC时间戳,以毫秒为单位。
+ */
+ private Long purchaseTime;
+
+ /**
+ * 订单交易状态。
+ *
+ * -1:初始化
+ * 0:已购买
+ * 1:已取消
+ * 2:已撤销或已退款
+ * 3:待处理
+ */
+ private Integer purchaseState;
+
+ /**
+ *
+ * 商户侧保留信息,由您在调用支付接口时传入。
+ */
+ private String developerPayload;
+
+ /**
+ *消耗状态,仅一次性商品存在,取值包括:
+ *
+ * 0:未消耗
+ * 1:已消耗
+ */
+ private Integer consumptionState;
+
+ /**
+ *确认状态,取值包括:
+ *
+ * 0 :未确认
+ * 1:已确认
+ * 没有值表示不需要确认
+ */
+ private Integer confirmed;
+
+ /**
+ * 用于唯一标识商品和用户对应关系的购买令牌,在支付完成时由华为应用内支付服务器生成。
+ * 说明
+ * 该字段是唯一标识商品和用户对应关系的,在订阅型商品正常续订时不会改变。
+ * 当前92位,后续存在扩展可能,如要进行存储,建议您预留128位的长度。
+ * 如要进行存储,为保证安全,建议加密存储。
+ */
+ private String purchaseToken;
+
+ /**
+ * 用定价货币的币种,请参见ISO 4217标准。
+ * 说明
+ * 为避免资金损失,您在对支付结果验签成功后,必须对其进行校验。
+ */
+ private String currency;
+
+ /**
+ * 商品实际价格*100以后的值。商品实际价格精确到小数点后2位,例如此参数值为501,则表示商品实际价格为5.01。
+ */
+ private Long price;
+
+ /**
+ * 支付方式,取值请参见payType说明。
+ */
+ private String payType;
+
+ /**
+ * 交易单号,用户支付成功后生成。
+ */
+ private String payOrderId;
+
+ // 以下参数只在订阅场景返回
+
+
+ /**
+ * 上次续期收款的订单ID,由支付服务器在续期扣费时生成。首次购买订阅型商品时的lastOrderId与orderId数值相同。
+ */
+ private String lastOrderId;
+
+ /**
+ * 订阅型商品所属的订阅组ID。
+ */
+ private String productGroup;
+
+ /**
+ * 原购买的时间,即本订阅型商品首次成功收费的时间,UTC时间戳,以毫秒为单位。。
+ */
+ private Long oriPurchaseTime;
+
+ /**
+ * 订阅ID。
+ * 说明
+ * subscriptionId是用户与商品之间的一一对应关系,在订阅型商品正常续订时不会改变。
+ */
+ private String subscriptionId;
+
+ /**
+ * 原订阅ID。有值表示当前订阅是从其他商品切换来的,该值可以关联切换前的商品订阅信息
+ */
+ private String oriSubscriptionId;
+
+ /**
+ * 购买数量。
+ */
+ private Integer quantity;
+
+ /**
+ * 已经付费订阅的天数,免费试用和促销期周期除外。。
+ */
+ private Long daysLasted;
+
+ /**
+ * 成功标准续期(没有设置促销的续期)的期数,为0或者不存在表示还没有成功续期。
+ */
+ private Long numOfPeriods;
+
+ /**
+ * 成功促销续期期数。
+ */
+ private Long numOfDiscount;
+
+ /**
+ * 订阅型商品过期时间,UTC时间戳,以毫秒为单位。
+ *
+ * 对于一个成功收费的自动续订收据,该时间表示续期日期或者超期日期。如果商品最近的收据的该时间是一个过去的时间,则订阅已经过期。
+ */
+ private Long expirationDate;
+
+ /**
+ * 对于已经过期的订阅,表示过期原因,取值包括:
+ *
+ * 1:用户取消
+ * 2:商品不可用
+ * 3:用户签约信息异常
+ * 4:Billing错误
+ * 5:用户未同意涨价
+ * 6:未知错误
+ * 同时有多个异常时,优先级为:1 > 2 > 3…
+ */
+ private Integer expirationIntent;
+
+ /**
+ * 订阅撤销时间,UTC时间戳,以毫秒为单位,发生退款且服务立即不可用。在顾客投诉,通过客服撤销订阅,或者顾客升级、跨级到同组其他商品并且立即生效场景下,需要撤销原有订阅的上次收据时有值。
+ * 说明
+ * 已经撤销的收据等同于没有完成购买。
+ */
+ private Long cancelTime;
+
+ /**
+ * 取消原因。
+ *
+ * 3: 应用调用IAP接口取消
+ * 2:顾客升级、跨级等。
+ * 1:顾客因为在App内遇到了问题而取消了订阅。
+ * 0:其他原因取消,比如顾客错误地订阅了商品。
+ * 说明
+ * 如果为空且cancelTime有值,表示是升级等操作导致的取消。
+ */
+ private Integer cancelReason;
+
+ /**
+ * 续期状态。
+ *
+ * 1:当前周期到期时自动续期
+ * 0:用户停止了续期
+ * 仅针对自动续期订阅,对有效和过期的订阅均有效,并不代表顾客的订阅状态。通常,取值为0时,应用可以给顾客提供其他的订阅选项,
+ * 例如推荐一个同组更低级别的商品。该值为0通常代表着顾客主动取消了该订阅。
+ */
+ private Integer renewStatus;
+
+ /**
+ * 用户取消订阅的时间,UTC时间戳,以毫秒为单位。在该时间进行了订阅续期停止的设定,商品在有效期内仍然有效,但后续的续期会终止,无退款。
+ */
+ private Integer cancellationTime;
+
+
+
+}
diff --git a/sf-payment/src/main/java/com/sf/payment/domain/PurchaseOrderPayload.java b/sf-payment/src/main/java/com/sf/payment/domain/PurchaseOrderPayload.java
new file mode 100644
index 0000000..8c8fab2
--- /dev/null
+++ b/sf-payment/src/main/java/com/sf/payment/domain/PurchaseOrderPayload.java
@@ -0,0 +1,165 @@
+package com.sf.payment.domain;
+
+import lombok.Data;
+
+/**
+ * 功能描述:
+ * 订单信息模型,支持消耗型商品、非消耗型商品和自动续期订阅商品。
+ * @author a_kun
+ * @date 2024/4/16 15:29
+ */
+@Data
+public class PurchaseOrderPayload {
+
+ /**
+ * 具体一笔订单中对应的购买订单号ID。
+ */
+ private String purchaseOrderId;
+
+ /**
+ * 购买token,
+ * 在购买消耗型/非消耗型商品场景中与具体购买订单一一对应,
+ * 在自动续期订阅商品场景中与订阅ID一一对应。
+ */
+ private String purchaseToken;
+
+ /**
+ * 应用ID。
+ */
+ private String applicationId;
+
+ /**
+ * 商品ID。每种商品必须有唯一的ID,由应用在PMS中维护,或者应用发起购买时传入。
+ * 说明
+ * 为避免资金损失,您在对支付结果验签成功后,必须对其进行校验。
+ */
+ private String productId;
+
+ /**
+ * 商品类型。具体取值如下:
+ * • 0:消耗型商品
+ * • 1:非消耗型商品
+ * • 2:自动续期订阅商品
+ */
+ private Integer productType;
+
+ /**
+ * 购买时间,UTC时间戳,以毫秒为单位。
+ * 如果没有完成购买,则没有值。
+ */
+ private Long purchaseTime;
+
+ /**
+ * 发货状态。具体取值如下:
+ * • 1:已发货
+ * • 2:未发货
+ */
+ private Integer finishStatus;
+
+ /**
+ * 价格,单位:分。
+ * 实际价格*100以后的值。商品实际价格精确到小数点后2位,例如此参数值为501,则表示商品实际价格为5.01。
+ */
+ private Long price;
+
+ /**
+ * 用定价货币的币种,请参见ISO 4217标准。
+ * 说明
+ * 为避免资金损失,您在对支付结果验签成功后,必须对其进行校验。
+ */
+ private String currency;
+
+ /**
+ * 商户侧保留信息,由您在调用支付接口时传入。
+ */
+ private String developerPayload;
+
+ /**
+ * 购买订单撤销原因。
+ * • 0:其他
+ * • 1:用户遇到问题退款
+ */
+ private Integer purchaseOrderRevocationReasonCode;
+
+ /**
+ * 购买订单撤销时间,
+ * UTC时间戳,以毫秒为单位。
+ */
+ private Long revocationTime;
+
+ /**
+ * 优惠类型。
+ * • 1:推介促销
+ */
+ private Integer offerTypeCode;
+
+ /**
+ * 优惠ID。
+ */
+ private String offerId;
+
+ /**
+ * 国家/地区码,用于区分国家/地区信息,请参见ISO 3166
+ */
+ private String countryCode;
+
+ /**
+ * 签名时间,UTC时间戳,以毫秒为单位。
+ */
+ private Long signedTime;
+
+ // 以下参数只在自动续期订阅商品场景返回
+
+
+ /**
+ * 订阅连续购买段的唯一ID,
+ * 当用户切换商品不会重置此ID。
+ */
+ private String subGroupGenerationId;
+
+ /**
+ * 订阅连续购买段的唯一ID,
+ * 当用户切换订阅商品时
+ * 此订阅ID会发生改变。
+ */
+ private String subscriptionId;
+
+ /**
+ * 订阅组ID。
+ */
+ private String subGroupId;
+
+ /**
+ * 此次购买的有效周期,
+ * 采用ISO 8601格式。
+ * 例如:P1W表示一周,
+ * P1M表示一个月。
+ */
+ private String duration;
+
+ /**
+ * 订阅周期段类型。
+ * • 0:正常周期段
+ * • 1:延期周期段
+ */
+ private Integer durationTypeCode;
+
+ /*
+ ISO 8601的时间持续期限表示
+ 在ISO 8601中,时间持续期限的表示采用了一种简洁而明确的格式,例如 “P10D”,其中 “P” 表示周期(Period),后面的数字表示周期的长度,而末尾的字母表示周期的单位。这种表示法主要用于描述时间段的长度,而不关注具体的时刻。
+ “P” 表示周期(Period): 此字母指示接下来的时间表示将是一个时间段的描述,而非具体的日期或时刻。
+ 后面的数字: 这个数字表示时间段的长度,可以是整数或小数。它指示了在时间单位内的周期数量。
+ 末尾的字母表示周期的单位: 在 “P10D” 中,末尾的 “D” 表示周期的单位是天(Days)。ISO 8601定义了多种可能的时间单位,包括:
+ Y(年): 表示年份,例如 “P2Y” 表示2年的时间段。
+ M(月): 表示月份,例如 “P3M” 表示3个月的时间段。
+ W(周): 表示周数,例如 “P1W” 表示1周的时间段。
+ D(日): 表示天数,例如 “P10D” 表示10天的时间段。
+ T(时间分隔符): 如果时间段中包含了时间信息,日期和时间之间用 “T” 分隔,例如 “P1DT12H” 表示1天12小时的时间段。
+ H(小时)、M(分钟)、S(秒): 用于表示时、分、秒的时间段长度,例如 “PT2H30M” 表示2小时30分钟的时间段。
+ 示例:
+ “P1Y”: 表示1年的时间段。
+ “P3M”: 表示3个月的时间段。
+ “P2W”: 表示2周的时间段。
+ “P4DT6H30M”: 表示4天6小时30分钟的时间段。
+ */
+}
diff --git a/sf-payment/src/main/java/com/sf/payment/domain/SubGroupStatusPayload.java b/sf-payment/src/main/java/com/sf/payment/domain/SubGroupStatusPayload.java
new file mode 100644
index 0000000..7fcad45
--- /dev/null
+++ b/sf-payment/src/main/java/com/sf/payment/domain/SubGroupStatusPayload.java
@@ -0,0 +1,48 @@
+package com.sf.payment.domain;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 功能描述:
+ * 已购订阅相关状态信息
+ * @author a_kun
+ * @date 2024/4/16 15:29
+ */
+@Data
+public class SubGroupStatusPayload {
+
+ /**
+ * 应用ID。
+ */
+ private String applicationId;
+
+ /**
+ * 应用包名。
+ */
+ private String packageName;
+
+ /**
+ * 订阅组ID。
+ */
+ private String subGroupId;
+
+ /**
+ * 订阅组中最后生效的
+ * 订阅状态
+ * SubscriptionStatus,
+ * 比如A切换B,B切换C,
+ * 此处是C的订阅状态。
+ */
+ private SubscriptionStatus lastSubscriptionStatus;
+
+ /**
+ * 订阅组最近生效的
+ * 历史订阅状态
+ * SubscriptionStatus的列表,比如A切换B,B切换C,这里包含C,B,A三个订阅状态信息。
+ */
+ private List historySubscriptionStatusList;
+
+
+}
diff --git a/sf-payment/src/main/java/com/sf/payment/domain/SubRenewalInfo.java b/sf-payment/src/main/java/com/sf/payment/domain/SubRenewalInfo.java
new file mode 100644
index 0000000..cfff488
--- /dev/null
+++ b/sf-payment/src/main/java/com/sf/payment/domain/SubRenewalInfo.java
@@ -0,0 +1,91 @@
+package com.sf.payment.domain;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 功能描述:
+ * 当前订阅最新的未来扣费计划
+ * @author a_kun
+ * @date 2024/4/16 15:29
+ */
+@Data
+public class SubRenewalInfo {
+
+ /**
+ * 订阅连续购买段的唯一ID,
+ * 当用户切换商品不会重置此ID。
+ */
+ private String subGroupGenerationId;
+
+ /**
+ * 下周期生效场景下,下期将续期的商品ID。
+ */
+ private String nextRenewPeriodProductId;
+
+ /**
+ * 当前生效的商品ID。
+ */
+ private String productId;
+
+ /**
+ * 自动续期状态。
+ * • 0:关闭
+ * • 1:打开
+ */
+ private Integer autoRenewStatusCode;
+
+ /**
+ * 系统是否还在尝试扣费。
+ * • true:是
+ * • false:否
+ */
+ private Boolean hasInBillingRetryPeriod;
+
+ /**
+ * 目前涨价状态码。
+ * • 1:用户暂未同意涨价
+ * • 2:用户已同意涨价
+ */
+ private Integer priceIncreaseStatusCode;
+
+ /**
+ * 优惠类型。
+ * • 1:推介促销
+ */
+ private Integer offerTypeCode;
+
+ /**
+ * 优惠ID。
+ */
+ private String offerId;
+
+ /**
+ * 下期续费价格,单位:分,取消订阅场景下不返回。
+ */
+ private String renewalPrice;
+
+ /**
+ * 币种。
+ */
+ private String currency;
+
+ /**
+ * 续期时间,UTC时间戳,以毫秒为单位。。
+ */
+ private Long renewalTime;
+
+ /**
+ * 订阅续期失败的原因。
+ * • 1:用户取消
+ * • 2:商品无效
+ * • 3:签约无效
+ * • 4:扣费异常
+ * • 5:用户不同意涨价
+ * • 6:未知
+ */
+ private String expirationIntent;
+
+
+}
diff --git a/sf-payment/src/main/java/com/sf/payment/domain/SubscriptionStatus.java b/sf-payment/src/main/java/com/sf/payment/domain/SubscriptionStatus.java
new file mode 100644
index 0000000..8adb949
--- /dev/null
+++ b/sf-payment/src/main/java/com/sf/payment/domain/SubscriptionStatus.java
@@ -0,0 +1,68 @@
+package com.sf.payment.domain;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 功能描述:
+ * 订阅组中最后生效的订阅状态
+ * @author a_kun
+ * @date 2024/4/16 15:29
+ */
+@Data
+public class SubscriptionStatus {
+
+ /**
+ * 订阅连续购买段的唯一ID,
+ * 当用户切换商品不会重置此ID。
+ */
+ private String subGroupGenerationId;
+
+ /**
+ * 订阅连续购买段的唯一ID,
+ * 当用户切换订阅商品时
+ * 此订阅ID会发生改变。
+ */
+ private String subscriptionId;
+
+ /**
+ * 购买token,
+ * 在购买消耗型/非消耗型商品场景中与具体购买订单一一对应,
+ * 在自动续期订阅商品场景中与订阅ID一一对应。
+ */
+ private String purchaseToken;
+
+ /**
+ * 订阅状态。
+ * • 1:生效状态
+ * • 2:已到期
+ * • 3:尝试扣费
+ * • 5:撤销
+ * • 6:暂停
+ */
+ private Integer status;
+
+ /**
+ * 自动续期订阅商品的过期时间,UTC时间戳,以毫秒为单位。
+ */
+ private Long expiresTime;
+
+ /**
+ * 当前订阅最新的一笔购买订单。包含的参数请参见PurchaseOrderPayload。
+ */
+ private PurchaseOrderPayload lastPurchaseOrder;
+
+ /**
+ * 当前订阅最新的购买订单列表,包含续期、延期、折算等产生的购买订单。
+ * 购买订单包含的参数请参见PurchaseOrderPayload。
+ */
+ private List recentPurchaseOrderList;
+
+ /**
+ * 当前订阅最新的未来扣费计划,包含的参数请参见SubRenewalInfo。
+ */
+ private SubRenewalInfo renewalInfo;
+
+
+}
diff --git a/sf-payment/src/main/java/com/sf/payment/service/IHuaweiPaymentService.java b/sf-payment/src/main/java/com/sf/payment/service/IHuaweiPaymentService.java
new file mode 100644
index 0000000..1383be3
--- /dev/null
+++ b/sf-payment/src/main/java/com/sf/payment/service/IHuaweiPaymentService.java
@@ -0,0 +1,14 @@
+package com.sf.payment.service;
+
+
+import com.sf.payment.domain.HuaweiPurchasesVerifyDTO;
+
+/**
+ * 功能描述:
+ *
+ * @author a_kun
+ * @date 2024/4/12 10:21
+ */
+public interface IHuaweiPaymentService {
+ void purchasesVerify(HuaweiPurchasesVerifyDTO verifyDTO);
+}
diff --git a/sf-payment/src/main/java/com/sf/payment/service/impl/HuaweiPaymentServiceImpl.java b/sf-payment/src/main/java/com/sf/payment/service/impl/HuaweiPaymentServiceImpl.java
new file mode 100644
index 0000000..473d026
--- /dev/null
+++ b/sf-payment/src/main/java/com/sf/payment/service/impl/HuaweiPaymentServiceImpl.java
@@ -0,0 +1,403 @@
+package com.sf.payment.service.impl;
+
+import cn.hutool.core.date.DateField;
+import cn.hutool.core.date.DateTime;
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.http.HttpUtil;
+import com.alibaba.fastjson2.JSON;
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.interfaces.DecodedJWT;
+import com.sf.common.enums.RequestHeaderEnums;
+import com.sf.common.utils.DateUtils;
+import com.sf.common.utils.SecurityUtils;
+import com.sf.common.utils.SnowflakeIdWorker;
+import com.sf.common.utils.http.RequestUtils;
+import com.sf.order.domain.OrderInfo;
+import com.sf.order.service.IOrderInfoService;
+import com.sf.payment.config.HuaweiPaymentConfig;
+import com.sf.payment.constant.GoodsConstants;
+import com.sf.payment.domain.*;
+import com.sf.payment.service.IHuaweiPaymentService;
+import com.sf.payment.utils.HuaweiTokenGenerator;
+import com.sf.service.domain.GoodsMessages;
+import com.sf.service.service.IGoodsMessagesService;
+import com.sf.system.domain.UserMember;
+import com.sf.system.service.IUserMemberService;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.codec.binary.Base64;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.Assert;
+import org.springframework.util.Base64Utils;
+
+import javax.annotation.Resource;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyFactory;
+import java.security.MessageDigest;
+import java.security.PublicKey;
+import java.security.Security;
+import java.security.spec.X509EncodedKeySpec;
+import java.text.MessageFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+
+/**
+ * 功能描述:
+ *
+ * @author a_kun
+ * @date 2024/4/12 10:21
+ */
+@Slf4j
+@Service
+public class HuaweiPaymentServiceImpl implements IHuaweiPaymentService {
+
+ /**
+ * token url to get the authorization
+ */
+ private static final String TOKEN_URL = "https://oauth-login.cloud.huawei.com/oauth2/v3/token";
+
+ /**
+ * 查询消耗型/非消耗型商品的订单最新状态。
+ */
+ private static final String ORDER_STATUS_QUERY_URL = "/order/harmony/v1/application/order/status/query";
+
+ /**
+ * 查询自动续期订阅商品的最新状态。
+ */
+ private static final String SUBSCRIPTION_STATUS_QUERY_URL = "/subscription/harmony/v1/application/subscription/status/query";
+
+ /**
+ * 站点信息。(中国)
+ */
+ private static final String ROOT_URL = "https://iap.cloud.huawei.com";
+
+ private static final String PUBLIC_KEY = "PUBLIC_KEY";
+
+ @Resource
+ private IOrderInfoService orderInfoService;
+
+ @Resource
+ private IUserMemberService userMemberService;
+
+ @Resource
+ private IGoodsMessagesService goodsMessagesService;
+
+ @Resource
+ private HuaweiPaymentConfig huaweiPaymentConfig;
+
+ @Resource
+ private SnowflakeIdWorker snowflakeIdWorker;
+
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void purchasesVerify(HuaweiPurchasesVerifyDTO verifyDTO) {
+ // 验证 TODO 证书验签官网未实现
+ if (GoodsConstants.GOODS_TYPE_CONSUMABLE.equals(verifyDTO.getType())
+ || GoodsConstants.GOODS_TYPE_NON_CONSUMABLE.equals(verifyDTO.getType())) {
+ Assert.hasText(verifyDTO.getJwsPurchaseOrder(), "订单信息不能为空");
+ // 消耗/非消耗商品购买验证
+ consumablePurchasesVerify(verifyDTO.getJwsPurchaseOrder());
+ } else if (GoodsConstants.GOODS_TYPE_AUTOMATIC_RENEWAL_SUBSCRIPTION.equals(verifyDTO.getType())) {
+ Assert.hasText(verifyDTO.getJwsSubscriptionStatus(), "订单信息不能为空");
+ // 订阅商品购买验证
+ subscriptionPurchasesVerify(verifyDTO.getJwsSubscriptionStatus());
+ } else {
+ throw new IllegalArgumentException("商品类型错误!");
+ }
+ }
+
+ private PurchaseOrderPayload subscriptionPurchasesVerify(String jwsSubscriptionStatus) {
+ DecodedJWT decodedJWT = JWT.decode(jwsSubscriptionStatus);
+ //String header = decodedJWT.getHeader();
+ String payload = decodedJWT.getPayload();
+ // 前面应该不是base64编码后的,官网说都是编码后的,但是解码会报错
+ //String signature = decodedJWT.getSignature();
+ String decodeAppPayload = new String(Base64Utils.decode(payload.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
+ // String decodeHeader = new String(Base64Utils.decode(header.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
+ // String decodeSignature = new String(Base64Utils.decode(signature.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
+ SubGroupStatusPayload appSubGroupStatusPayload = JSON.parseObject(decodeAppPayload, SubGroupStatusPayload.class);
+ SubscriptionStatus lastSubscriptionStatus = appSubGroupStatusPayload.getLastSubscriptionStatus();
+ PurchaseOrderPayload lastPurchaseOrder = lastSubscriptionStatus.getLastPurchaseOrder();
+ // pack the request body
+ Map bodyMap = new HashMap<>();
+ bodyMap.put("purchaseToken", lastPurchaseOrder.getPurchaseToken());
+ bodyMap.put("purchaseOrderId", lastPurchaseOrder.getPurchaseOrderId());
+ // construct the Authorization in Header
+ Map headers = buildAuthorization(huaweiPaymentConfig.getAppId(), bodyMap);
+ log.info("调用华为主动续期入惨:{}", JSON.toJSONString(bodyMap));
+ // 订阅状态查询
+ String response = HttpUtil.createPost(ROOT_URL + SUBSCRIPTION_STATUS_QUERY_URL)
+ .addHeaders(headers)
+ .body(JSON.toJSONString(bodyMap))
+ .execute().body();
+ log.info("订单状态查询返回信息:{}", response);
+ HuaweiQueryResponse huaweiQueryResponse = JSON.parseObject(response, HuaweiQueryResponse.class);
+ if (!"0".equals(huaweiQueryResponse.getResponseCode())) {
+ throw new RuntimeException("订单状态查询失败");
+ }
+ DecodedJWT huaweiQueryResponseDecodedJWT = JWT.decode(huaweiQueryResponse.getJwsSubGroupStatus());
+ String huaweiQueryResponsepayload = huaweiQueryResponseDecodedJWT.getPayload();
+ String decodeHuaweiQueryResponsepayload = new String(Base64Utils.decode(huaweiQueryResponsepayload.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
+ SubGroupStatusPayload subGroupStatusPayload = JSON.parseObject(decodeHuaweiQueryResponsepayload, SubGroupStatusPayload.class);
+ PurchaseOrderPayload huaweiQueryResponsePurchaseOrderPayload = subGroupStatusPayload.getLastSubscriptionStatus().getLastPurchaseOrder();
+ // 发货
+ return delivery(lastPurchaseOrder, huaweiQueryResponsePurchaseOrderPayload, subGroupStatusPayload.getLastSubscriptionStatus());
+ }
+
+ /**
+ * 发货
+ *
+ * @param purchaseOrder
+ * @param huaweiQueryResponsePurchaseOrderPayload
+ * @param lastSubscriptionStatus
+ * @return
+ */
+ private PurchaseOrderPayload delivery(PurchaseOrderPayload purchaseOrder, PurchaseOrderPayload huaweiQueryResponsePurchaseOrderPayload, SubscriptionStatus lastSubscriptionStatus) {
+ Assert.isTrue(purchaseOrder.getPurchaseOrderId().equals(huaweiQueryResponsePurchaseOrderPayload.getPurchaseOrderId()), "订单不一致,发货失败!");
+ if (2 == huaweiQueryResponsePurchaseOrderPayload.getFinishStatus()) {
+ // 还未发货
+ // 查询平台是否配置该商品
+ GoodsMessages goods = goodsMessagesService.selectGoodsMessagesByCode(huaweiQueryResponsePurchaseOrderPayload.getProductId());
+ Assert.notNull(goods, "未配置此商品,请检查商品配置");
+ Assert.isTrue(goods.getOriginalPrice().equals(huaweiQueryResponsePurchaseOrderPayload.getPrice()), "商品价格与订单价格不一致,请检查价格配置");
+ // 创建完成订单
+ createOrder(huaweiQueryResponsePurchaseOrderPayload, goods);
+ // 发放会员权益
+ distributeMembershipBenefits(huaweiQueryResponsePurchaseOrderPayload, lastSubscriptionStatus);
+ } else {
+ log.info("华为应用内支付订单已发货!{}", JSON.toJSONString(huaweiQueryResponsePurchaseOrderPayload));
+ }
+ return huaweiQueryResponsePurchaseOrderPayload;
+ }
+
+ private PurchaseOrderPayload consumablePurchasesVerify(String jwsPurchaseOrder) {
+ DecodedJWT decodedJWT = JWT.decode(jwsPurchaseOrder);
+ //String header = decodedJWT.getHeader();
+ String payload = decodedJWT.getPayload();
+ // 前面应该不是base64编码后的,官网说都是编码后的,但是解码会报错
+ //String signature = decodedJWT.getSignature();
+ String decodeAppPayload = new String(Base64Utils.decode(payload.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
+ // String decodeHeader = new String(Base64Utils.decode(header.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
+ // String decodeSignature = new String(Base64Utils.decode(signature.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
+ PurchaseOrderPayload appPurchaseOrderPayload = JSON.parseObject(decodeAppPayload, PurchaseOrderPayload.class);
+ // pack the request body
+ Map bodyMap = new HashMap<>();
+ bodyMap.put("purchaseToken", appPurchaseOrderPayload.getPurchaseToken());
+ bodyMap.put("purchaseOrderId", appPurchaseOrderPayload.getPurchaseOrderId());
+ // construct the Authorization in Header
+ Map headers = buildAuthorization(huaweiPaymentConfig.getAppId(), bodyMap);
+
+ // 订单状态查询
+ String response = HttpUtil.createPost(ROOT_URL + ORDER_STATUS_QUERY_URL)
+ .addHeaders(headers)
+ .body(JSON.toJSONString(bodyMap))
+ .execute().body();
+ log.info("订单状态查询返回信息:{}", response);
+ HuaweiQueryResponse huaweiQueryResponse = JSON.parseObject(response, HuaweiQueryResponse.class);
+ if (!"0".equals(huaweiQueryResponse.getResponseCode())) {
+ throw new RuntimeException("订单状态查询失败");
+ }
+ DecodedJWT huaweiQueryResponseDecodedJWT = JWT.decode(huaweiQueryResponse.getJwsPurchaseOrder());
+ // String huaweiQueryResponseHeader = huaweiQueryResponseDecodedJWT.getHeader();
+ String huaweiQueryResponsepayload = huaweiQueryResponseDecodedJWT.getPayload();
+ // String huaweiQueryResponsesignature = huaweiQueryResponseDecodedJWT.getSignature();
+
+ //String decodehuaweiQueryResponseHeader = new String(Base64Utils.decode(huaweiQueryResponseHeader.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
+ String decodehuaweiQueryResponsepayload = new String(Base64Utils.decode(huaweiQueryResponsepayload.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
+ // String decodehuaweiQueryResponsesignature = new String(Base64Utils.decode(huaweiQueryResponsesignature.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
+
+ PurchaseOrderPayload huaweiQueryResponsePurchaseOrderPayload = JSON.parseObject(decodehuaweiQueryResponsepayload, PurchaseOrderPayload.class);
+ // 暂时只做简单验证
+ return delivery(appPurchaseOrderPayload, huaweiQueryResponsePurchaseOrderPayload, null);
+ }
+
+ private void createOrder(PurchaseOrderPayload appPurchaseOrderPayload, GoodsMessages goods) {
+ Long userId = SecurityUtils.getUserId();
+ OrderInfo orderInfo = new OrderInfo();
+ orderInfo.setAppCode(RequestUtils.getHeader(RequestHeaderEnums.APP_CODE.getCode()));
+ orderInfo.setOrderNo(snowflakeIdWorker.nextId());
+ orderInfo.setOrderStatus(4L);
+ orderInfo.setPayType(1L);
+ orderInfo.setPayChannel(2L);
+ orderInfo.setOrderAmt(appPurchaseOrderPayload.getPrice());
+ orderInfo.setPayAmt(appPurchaseOrderPayload.getPrice());
+ orderInfo.setReallyAmt(appPurchaseOrderPayload.getPrice());
+ orderInfo.setReceiveType(0L);
+ orderInfo.setGoodsId(goods.getId());
+ orderInfo.setPayTime(DateUtil.date(appPurchaseOrderPayload.getPurchaseTime()));
+ orderInfo.setCreateUserId(userId);
+ orderInfo.setUpdateUserId(userId);
+ orderInfo.setOutOrderNo(appPurchaseOrderPayload.getPurchaseOrderId());
+ orderInfo.setPayData(JSON.toJSONString(appPurchaseOrderPayload));
+ orderInfo.setGoodsPrice(goods.getOriginalPrice());
+ orderInfo.setGoodsType(goods.getGoodsType());
+ orderInfo.setGoodsCode(goods.getGoodsCode());
+ orderInfo.setCreateTime(DateUtils.getNowDate());
+ orderInfo.setUpdateTime(DateUtils.getNowDate());
+ orderInfoService.insertOrder(orderInfo);
+
+ }
+
+ private void distributeMembershipBenefits(PurchaseOrderPayload purchaseOrderPayload, SubscriptionStatus lastSubscriptionStatus) {
+ // 发放会员权益
+ Long userId = SecurityUtils.getUserId();
+ UserMember userMember = userMemberService.selectUserMemberByUserId(userId);
+ boolean isSubscription = GoodsConstants.GOODS_TYPE_AUTOMATIC_RENEWAL_SUBSCRIPTION.equals(purchaseOrderPayload.getProductType());
+ int subscriptionStatus = isSubscription ? lastSubscriptionStatus.getStatus() : 0;
+ if (userMember == null) {
+ // 添加会员信息
+ DateTime payTime = DateUtil.date(purchaseOrderPayload.getPurchaseTime());
+ userMember = new UserMember();
+ userMember.setMemberLevel(isSubscription ? 1 : 2);
+ userMember.setSubscriptionStatus(subscriptionStatus);
+ userMember.setUserId(userId);
+ userMember.setIntegration(0L);
+ DateTime expirationTime = DateUtil.offset(payTime, DateField.MONTH, 1);
+ userMember.setExpirationTime(expirationTime);
+ userMember.setCreateTime(new Date());
+ userMember.setUpdateTime(new Date());
+ userMemberService.insertUserMember(userMember);
+ } else {
+ // 更新
+ userMember.setMemberLevel(isSubscription ? 1 : 2);
+ userMember.setSubscriptionStatus(subscriptionStatus);
+ DateTime expirationTime = DateUtil.offset(userMember.getExpirationTime(), DateField.MONTH, 1);
+ userMember.setExpirationTime(expirationTime);
+ userMember.setUpdateTime(new Date());
+ userMemberService.updateUserMember(userMember);
+ }
+ }
+
+
+ /**
+ * Build Authorization in Header
+ *
+ * @return headers
+ */
+ public static Map buildAuthorization(String appId, Map body) {
+ Map jwtHeader = new HashMap<>(8);
+ jwtHeader.put("alg", "ES256");
+ jwtHeader.put("typ", "JWT");
+ jwtHeader.put("kid", "0ae3e1be-374b-43a5-a297-045addbf76eb");
+ Map jwtPayload = new HashMap<>(8);
+ jwtPayload.put("iss", "f59509e6-dd17-4644-b832-ff05233146c8");
+ jwtPayload.put("aud", "iap-v1");
+ jwtPayload.put("iat", DateUtil.currentSeconds());
+ jwtPayload.put("exp", DateUtil.currentSeconds() + 1800L); // 半小时过期
+ jwtPayload.put("aid", appId);
+ jwtPayload.put("digest", getJwtPayloadDigest(body));
+
+ String token = HuaweiTokenGenerator.createToken(jwtHeader, jwtPayload);
+ // String authorization = MessageFormat.format("Basic {0}", Base64.encodeBase64String(oriString.getBytes(StandardCharsets.UTF_8)));
+ String authorization = MessageFormat.format("Bearer {0}", token);
+ Map headers = new HashMap<>();
+ headers.put("Authorization", authorization);
+ headers.put("Content-Type", "application/json; charset=UTF-8");
+ return headers;
+ }
+
+ private static String getJwtPayloadDigest(Map body) {
+ try {
+ MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
+ messageDigest.update(JSON.toJSONString(body).getBytes(StandardCharsets.UTF_8));
+ byte[] digestByte = messageDigest.digest();
+ StringBuilder stringBuffer = new StringBuilder();
+ String temp;
+ for (byte aByte : digestByte) {
+ temp = Integer.toHexString(aByte & 0xFF);
+ if (temp.length() == 1) {
+ stringBuffer.append("0");
+ }
+ stringBuffer.append(temp);
+ }
+ return stringBuffer.toString();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * 校验签名信息,校验InAppPurchaseData中的productId、price、currency等信息的一致性
+ *
+ * @param content 结果字符串
+ * @param sign 签名字符串
+ * @param publicKey IAP公钥
+ * @return 是否校验通过
+ */
+ public static boolean checkSuccessOrder(String content, String sign, String publicKey, String signatureAlgorithm, OrderInfo orderInfo) {
+ // 校验签名信息
+ boolean checkRes = checkSign(content, sign, publicKey, StrUtil.blankToDefault(signatureAlgorithm, "SHA256WithRSA"));
+ if (checkRes) {
+ // 校验InAppPurchaseData中的productId、price、currency等信息的一致性
+ checkRes = checkProductIdAndPriceAndCurrency(content, orderInfo);
+ }
+ return checkRes;
+ }
+
+ /**
+ * 校验InAppPurchaseData中的productId、price、currency等信息的一致性
+ *
+ * @param content 结果字符串
+ * @param orderInfo 您的订单信息,包括productId、price、currency
+ * @return 是否校验通过
+ */
+ public static boolean checkProductIdAndPriceAndCurrency(String content, OrderInfo orderInfo) {
+ InAppPurchaseData inAppPurchaseData = JSON.parseObject(content, InAppPurchaseData.class);
+ // 校验InAppPurchaseData中的productId、price、currency等信息的一致性
+ return inAppPurchaseData.getProductId().equals(orderInfo.getGoodsId())
+ && inAppPurchaseData.getPrice().equals(orderInfo.getOrderAmt());
+ }
+
+ /**
+ * 校验签名信息
+ *
+ * @param content 结果字符串
+ * @param sign 签名字符串
+ * @param publicKey IAP公钥
+ * @param signatureAlgorithm 签名算法字段,可从接口返回数据中获取,例如:OwnedPurchasesResult.getSignatureAlgorithm()
+ * @return 是否校验通过
+ */
+ public static boolean checkSign(String content, String sign, String publicKey, String signatureAlgorithm) {
+ if (sign == null) {
+ return false;
+ }
+ if (publicKey == null) {
+ return false;
+ }
+
+ // 当signatureAlgorithm为空时使用默认签名算法
+ if (signatureAlgorithm == null || signatureAlgorithm.length() == 0) {
+ signatureAlgorithm = "SHA256WithRSA";
+ System.out.println("doCheck, algorithm: SHA256WithRSA");
+ }
+ try {
+ Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
+ // 生成"RSA"的KeyFactory对象
+ KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+ byte[] decodedKey = Base64.decodeBase64(publicKey);
+ // 生成公钥
+ PublicKey pubKey = keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
+ java.security.Signature signature = null;
+ // 根据SHA256WithRSA算法获取签名对象实例
+ signature = java.security.Signature.getInstance(signatureAlgorithm);
+ // 初始化验证签名的公钥
+ signature.initVerify(pubKey);
+ // 把原始报文更新到签名对象中
+ signature.update(content.getBytes(StandardCharsets.UTF_8));
+ // 将sign解码
+ byte[] bsign = Base64.decodeBase64(sign);
+ // 进行验签
+ return signature.verify(bsign);
+ } catch (RuntimeException e) {
+ throw e;
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return false;
+ }
+
+}
diff --git a/sf-payment/src/main/java/com/sf/payment/utils/HuaweiTokenGenerator.java b/sf-payment/src/main/java/com/sf/payment/utils/HuaweiTokenGenerator.java
new file mode 100644
index 0000000..4572ea6
--- /dev/null
+++ b/sf-payment/src/main/java/com/sf/payment/utils/HuaweiTokenGenerator.java
@@ -0,0 +1,43 @@
+package com.sf.payment.utils;
+import cn.hutool.core.io.resource.ClassPathResource;
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.algorithms.Algorithm;
+import org.apache.commons.io.IOUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.interfaces.ECPrivateKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.Base64;
+
+import java.util.Map;
+
+public class HuaweiTokenGenerator {
+
+ public static String createToken(Map jwtHeader, Map jwtPayload) {
+ try {
+ // AppGallery Connect 华为应用内支付配置密钥,下载私钥文件
+ ClassPathResource classPathResource = new ClassPathResource("IAPKey_0ae3e1be-374b-43a5-a297-045addbf76eb.p8");
+ InputStream IAPKeyStream = classPathResource.getStream();
+ String content = IOUtils.toString(IAPKeyStream, String.valueOf(StandardCharsets.UTF_8));
+ String privateKey = content
+ .replace("-----BEGIN PRIVATE KEY-----", "")
+ .replaceAll("\\R+", "")
+ .replace("-----END PRIVATE KEY-----", "");
+ KeyFactory keyFactory = KeyFactory.getInstance("EC");
+ byte[] privateKeyBytes = Base64.getDecoder().decode(privateKey);
+ PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
+ ECPrivateKey ecPrivateKey = (ECPrivateKey) keyFactory.generatePrivate(keySpec);
+ return JWT.create()
+ .withHeader(jwtHeader)
+ .withPayload(jwtPayload)
+ .sign(Algorithm.ECDSA256(ecPrivateKey));
+ } catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/sf-payment/src/main/resources/IAPKey_0ae3e1be-374b-43a5-a297-045addbf76eb.p8 b/sf-payment/src/main/resources/IAPKey_0ae3e1be-374b-43a5-a297-045addbf76eb.p8
new file mode 100644
index 0000000..e1ebbeb
--- /dev/null
+++ b/sf-payment/src/main/resources/IAPKey_0ae3e1be-374b-43a5-a297-045addbf76eb.p8
@@ -0,0 +1,6 @@
+-----BEGIN PRIVATE KEY-----
+MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQggG04243qynU/yWYy
+XpYVy9ZWMuLKzZiwhXCBWQBCOLigCgYIKoZIzj0DAQehRANCAARNln2/d+TM2pIO
+LWQzvI77gPAVEvVCSlIuiJ+J7CJSG5KCysBaEeiiD5cc4dZWnUBijF8FBh7nDLaH
+VwFXfrS+
+-----END PRIVATE KEY-----
diff --git a/sf-service/src/main/java/com/sf/service/controller/GoodsMessagesController.java b/sf-service/src/main/java/com/sf/service/controller/GoodsMessagesController.java
new file mode 100644
index 0000000..624cca7
--- /dev/null
+++ b/sf-service/src/main/java/com/sf/service/controller/GoodsMessagesController.java
@@ -0,0 +1,110 @@
+package com.sf.service.controller;
+
+import java.util.List;
+import javax.servlet.http.HttpServletResponse;
+
+import apijson.orm.model.Request;
+import com.sf.common.enums.RequestHeaderEnums;
+import com.sf.common.utils.StringUtils;
+import com.sf.common.utils.http.RequestUtils;
+import com.sf.service.domain.GoodsMessages;
+import com.sf.service.service.IGoodsMessagesService;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.sf.common.annotation.Log;
+import com.sf.common.core.controller.BaseController;
+import com.sf.common.core.domain.AjaxResult;
+import com.sf.common.enums.BusinessType;
+import com.sf.common.utils.poi.ExcelUtil;
+import com.sf.common.core.page.TableDataInfo;
+
+/**
+ * 商品信息Controller
+ *
+ * @author zoukun
+ * @date 2024-04-18
+ */
+@RestController
+@RequestMapping("/service/goods")
+public class GoodsMessagesController extends BaseController
+{
+ @Autowired
+ private IGoodsMessagesService goodsMessagesService;
+
+ /**
+ * 查询商品信息列表
+ */
+ @GetMapping("/list")
+ public TableDataInfo list(GoodsMessages goodsMessages)
+ {
+ goodsMessages.setAppCode(RequestUtils.getHeader(RequestHeaderEnums.APP_CODE.getCode()));
+ startPage();
+ List list = goodsMessagesService.selectGoodsMessagesList(goodsMessages);
+ return getDataTable(list);
+ }
+
+ /**
+ * 导出商品信息列表
+ */
+ @PreAuthorize("@ss.hasPermi('service:goods:export')")
+ @Log(title = "商品信息", businessType = BusinessType.EXPORT)
+ @PostMapping("/export")
+ public void export(HttpServletResponse response, GoodsMessages goodsMessages)
+ {
+ List list = goodsMessagesService.selectGoodsMessagesList(goodsMessages);
+ ExcelUtil util = new ExcelUtil(GoodsMessages.class);
+ util.exportExcel(response, list, "商品信息数据");
+ }
+
+ /**
+ * 获取商品信息详细信息
+ */
+ @PreAuthorize("@ss.hasPermi('service:goods:query')")
+ @GetMapping(value = "/{id}")
+ public AjaxResult getInfo(@PathVariable("id") Long id)
+ {
+ return success(goodsMessagesService.selectGoodsMessagesById(id));
+ }
+
+ /**
+ * 新增商品信息
+ */
+ @PreAuthorize("@ss.hasPermi('service:goods:add')")
+ @Log(title = "商品信息", businessType = BusinessType.INSERT)
+ @PostMapping
+ public AjaxResult add(@RequestBody GoodsMessages goodsMessages)
+ {
+ goodsMessages.setAppCode(RequestUtils.getHeader(RequestHeaderEnums.APP_CODE.getCode()));
+ return toAjax(goodsMessagesService.insertGoodsMessages(goodsMessages));
+ }
+
+ /**
+ * 修改商品信息
+ */
+ @PreAuthorize("@ss.hasPermi('service:goods:edit')")
+ @Log(title = "商品信息", businessType = BusinessType.UPDATE)
+ @PutMapping
+ public AjaxResult edit(@RequestBody GoodsMessages goodsMessages)
+ {
+ return toAjax(goodsMessagesService.updateGoodsMessages(goodsMessages));
+ }
+
+ /**
+ * 删除商品信息
+ */
+ @PreAuthorize("@ss.hasPermi('service:goods:remove')")
+ @Log(title = "商品信息", businessType = BusinessType.DELETE)
+ @DeleteMapping("/{ids}")
+ public AjaxResult remove(@PathVariable Long[] ids)
+ {
+ return toAjax(goodsMessagesService.deleteGoodsMessagesByIds(ids));
+ }
+}
diff --git a/sf-service/src/main/java/com/sf/service/domain/GoodsMessages.java b/sf-service/src/main/java/com/sf/service/domain/GoodsMessages.java
new file mode 100644
index 0000000..19f9b50
--- /dev/null
+++ b/sf-service/src/main/java/com/sf/service/domain/GoodsMessages.java
@@ -0,0 +1,254 @@
+package com.sf.service.domain;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.sf.common.annotation.Excel;
+import com.sf.common.core.domain.BaseEntity;
+
+/**
+ * 商品信息对象 GOODS_MESSAGES
+ *
+ * @author zoukun
+ * @date 2024-04-18
+ */
+public class GoodsMessages extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+ /** 主键 */
+ private Long id;
+ /** 应用id */
+ private String appCode;
+
+ /** 商品编号 */
+ @Excel(name = "商品编号")
+ private String goodsCode;
+
+ /** 库存表编号 */
+ private Long stockId;
+
+ /** 审核状态,1通过,0未通过 */
+ @Excel(name = "审核状态,1通过,0未通过")
+ private Long reviewStatus;
+
+ /** 商品标题 */
+ @Excel(name = "商品标题")
+ private String productTitle;
+
+ /** 商品图片 */
+ @Excel(name = "商品图片")
+ private String productPicture;
+
+ /** 商品原价 */
+ @Excel(name = "商品原价")
+ private Long originalPrice;
+
+ /** 商品描述 */
+ @Excel(name = "商品描述")
+ private String productDesc;
+
+ /** 商品类型。 * • 0:消耗型商品 * • 1:非消耗型商品 * • 2:自动续期订阅商品 */
+ @Excel(name = "商品类型。 * • 0:消耗型商品 * • 1:非消耗型商品 * • 2:自动续期订阅商品")
+ private Integer goodsType;
+
+ /** 商品规格 */
+ @Excel(name = "商品规格")
+ private String goodsSpec;
+
+ /** 排序 */
+ private Long orderNum;
+
+ /** 逻辑删除,0:未删除,1:删除 */
+ private Long isDelete;
+
+ /** 创建人 */
+ private String created;
+
+ /** 更新人 */
+ private String modified;
+
+ /** 商品名称 */
+ private String goodsName;
+
+ /** 商品型号 */
+ @Excel(name = "商品型号")
+ private String goodsModel;
+
+ public void setId(Long id)
+ {
+ this.id = id;
+ }
+
+ public Long getId()
+ {
+ return id;
+ }
+ public void setGoodsCode(String goodsCode)
+ {
+ this.goodsCode = goodsCode;
+ }
+
+ public String getGoodsCode()
+ {
+ return goodsCode;
+ }
+ public void setStockId(Long stockId)
+ {
+ this.stockId = stockId;
+ }
+
+ public Long getStockId()
+ {
+ return stockId;
+ }
+ public void setReviewStatus(Long reviewStatus)
+ {
+ this.reviewStatus = reviewStatus;
+ }
+
+ public Long getReviewStatus()
+ {
+ return reviewStatus;
+ }
+ public void setProductTitle(String productTitle)
+ {
+ this.productTitle = productTitle;
+ }
+
+ public String getProductTitle()
+ {
+ return productTitle;
+ }
+ public void setProductPicture(String productPicture)
+ {
+ this.productPicture = productPicture;
+ }
+
+ public String getProductPicture()
+ {
+ return productPicture;
+ }
+ public void setOriginalPrice(Long originalPrice)
+ {
+ this.originalPrice = originalPrice;
+ }
+
+ public Long getOriginalPrice()
+ {
+ return originalPrice;
+ }
+ public void setProductDesc(String productDesc)
+ {
+ this.productDesc = productDesc;
+ }
+
+ public String getProductDesc()
+ {
+ return productDesc;
+ }
+ public void setGoodsType(Integer goodsType)
+ {
+ this.goodsType = goodsType;
+ }
+
+ public Integer getGoodsType()
+ {
+ return goodsType;
+ }
+ public void setGoodsSpec(String goodsSpec)
+ {
+ this.goodsSpec = goodsSpec;
+ }
+
+ public String getGoodsSpec()
+ {
+ return goodsSpec;
+ }
+ public void setOrderNum(Long orderNum)
+ {
+ this.orderNum = orderNum;
+ }
+
+ public Long getOrderNum()
+ {
+ return orderNum;
+ }
+ public void setIsDelete(Long isDelete)
+ {
+ this.isDelete = isDelete;
+ }
+
+ public Long getIsDelete()
+ {
+ return isDelete;
+ }
+ public void setCreated(String created)
+ {
+ this.created = created;
+ }
+
+ public String getCreated()
+ {
+ return created;
+ }
+ public void setModified(String modified)
+ {
+ this.modified = modified;
+ }
+
+ public String getModified()
+ {
+ return modified;
+ }
+ public void setGoodsName(String goodsName)
+ {
+ this.goodsName = goodsName;
+ }
+
+ public String getGoodsName()
+ {
+ return goodsName;
+ }
+ public void setGoodsModel(String goodsModel)
+ {
+ this.goodsModel = goodsModel;
+ }
+
+ public String getGoodsModel()
+ {
+ return goodsModel;
+ }
+
+ public String getAppCode() {
+ return appCode;
+ }
+
+ public void setAppCode(String appCode) {
+ this.appCode = appCode;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
+ .append("id", getId())
+ .append("appCode", getAppCode())
+ .append("goodsCode", getGoodsCode())
+ .append("stockId", getStockId())
+ .append("reviewStatus", getReviewStatus())
+ .append("productTitle", getProductTitle())
+ .append("productPicture", getProductPicture())
+ .append("originalPrice", getOriginalPrice())
+ .append("productDesc", getProductDesc())
+ .append("goodsType", getGoodsType())
+ .append("goodsSpec", getGoodsSpec())
+ .append("orderNum", getOrderNum())
+ .append("isDelete", getIsDelete())
+ .append("created", getCreated())
+ .append("modified", getModified())
+ .append("createTime", getCreateTime())
+ .append("updateTime", getUpdateTime())
+ .append("goodsName", getGoodsName())
+ .append("goodsModel", getGoodsModel())
+ .toString();
+ }
+}
diff --git a/sf-service/src/main/java/com/sf/service/mapper/GoodsMessagesMapper.java b/sf-service/src/main/java/com/sf/service/mapper/GoodsMessagesMapper.java
new file mode 100644
index 0000000..02a2f1a
--- /dev/null
+++ b/sf-service/src/main/java/com/sf/service/mapper/GoodsMessagesMapper.java
@@ -0,0 +1,63 @@
+package com.sf.service.mapper;
+
+import java.util.List;
+import com.sf.service.domain.GoodsMessages;
+
+/**
+ * 商品信息Mapper接口
+ *
+ * @author zoukun
+ * @date 2024-04-18
+ */
+public interface GoodsMessagesMapper
+{
+ /**
+ * 查询商品信息
+ *
+ * @param id 商品信息主键
+ * @return 商品信息
+ */
+ public GoodsMessages selectGoodsMessagesById(Long id);
+
+ /**
+ * 查询商品信息列表
+ *
+ * @param goodsMessages 商品信息
+ * @return 商品信息集合
+ */
+ public List selectGoodsMessagesList(GoodsMessages goodsMessages);
+
+ /**
+ * 新增商品信息
+ *
+ * @param goodsMessages 商品信息
+ * @return 结果
+ */
+ public int insertGoodsMessages(GoodsMessages goodsMessages);
+
+ /**
+ * 修改商品信息
+ *
+ * @param goodsMessages 商品信息
+ * @return 结果
+ */
+ public int updateGoodsMessages(GoodsMessages goodsMessages);
+
+ /**
+ * 删除商品信息
+ *
+ * @param id 商品信息主键
+ * @return 结果
+ */
+ public int deleteGoodsMessagesById(Long id);
+
+ /**
+ * 批量删除商品信息
+ *
+ * @param ids 需要删除的数据主键集合
+ * @return 结果
+ */
+ public int deleteGoodsMessagesByIds(Long[] ids);
+
+ GoodsMessages selectGoodsMessagesByCode(String goodsCode);
+}
diff --git a/sf-service/src/main/java/com/sf/service/service/IGoodsMessagesService.java b/sf-service/src/main/java/com/sf/service/service/IGoodsMessagesService.java
new file mode 100644
index 0000000..29f7dab
--- /dev/null
+++ b/sf-service/src/main/java/com/sf/service/service/IGoodsMessagesService.java
@@ -0,0 +1,63 @@
+package com.sf.service.service;
+
+import java.util.List;
+import com.sf.service.domain.GoodsMessages;
+
+/**
+ * 商品信息Service接口
+ *
+ * @author zoukun
+ * @date 2024-04-18
+ */
+public interface IGoodsMessagesService
+{
+ /**
+ * 查询商品信息
+ *
+ * @param id 商品信息主键
+ * @return 商品信息
+ */
+ public GoodsMessages selectGoodsMessagesById(Long id);
+
+ /**
+ * 查询商品信息列表
+ *
+ * @param goodsMessages 商品信息
+ * @return 商品信息集合
+ */
+ public List selectGoodsMessagesList(GoodsMessages goodsMessages);
+
+ /**
+ * 新增商品信息
+ *
+ * @param goodsMessages 商品信息
+ * @return 结果
+ */
+ public int insertGoodsMessages(GoodsMessages goodsMessages);
+
+ /**
+ * 修改商品信息
+ *
+ * @param goodsMessages 商品信息
+ * @return 结果
+ */
+ public int updateGoodsMessages(GoodsMessages goodsMessages);
+
+ /**
+ * 批量删除商品信息
+ *
+ * @param ids 需要删除的商品信息主键集合
+ * @return 结果
+ */
+ public int deleteGoodsMessagesByIds(Long[] ids);
+
+ /**
+ * 删除商品信息信息
+ *
+ * @param id 商品信息主键
+ * @return 结果
+ */
+ public int deleteGoodsMessagesById(Long id);
+
+ GoodsMessages selectGoodsMessagesByCode(String goodsCode);
+}
diff --git a/sf-service/src/main/java/com/sf/service/service/impl/GoodsMessagesServiceImpl.java b/sf-service/src/main/java/com/sf/service/service/impl/GoodsMessagesServiceImpl.java
new file mode 100644
index 0000000..0541ba8
--- /dev/null
+++ b/sf-service/src/main/java/com/sf/service/service/impl/GoodsMessagesServiceImpl.java
@@ -0,0 +1,105 @@
+package com.sf.service.service.impl;
+
+import java.util.List;
+import com.sf.common.utils.DateUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.sf.service.mapper.GoodsMessagesMapper;
+import com.sf.service.domain.GoodsMessages;
+import com.sf.service.service.IGoodsMessagesService;
+import org.springframework.util.StringUtils;
+
+/**
+ * 商品信息Service业务层处理
+ *
+ * @author zoukun
+ * @date 2024-04-18
+ */
+@Service
+public class GoodsMessagesServiceImpl implements IGoodsMessagesService
+{
+ @Autowired
+ private GoodsMessagesMapper goodsMessagesMapper;
+
+ /**
+ * 查询商品信息
+ *
+ * @param id 商品信息主键
+ * @return 商品信息
+ */
+ @Override
+ public GoodsMessages selectGoodsMessagesById(Long id)
+ {
+ return goodsMessagesMapper.selectGoodsMessagesById(id);
+ }
+
+ /**
+ * 查询商品信息列表
+ *
+ * @param goodsMessages 商品信息
+ * @return 商品信息
+ */
+ @Override
+ public List selectGoodsMessagesList(GoodsMessages goodsMessages)
+ {
+ return goodsMessagesMapper.selectGoodsMessagesList(goodsMessages);
+ }
+
+ /**
+ * 新增商品信息
+ *
+ * @param goodsMessages 商品信息
+ * @return 结果
+ */
+ @Override
+ public int insertGoodsMessages(GoodsMessages goodsMessages)
+ {
+ goodsMessages.setCreateTime(DateUtils.getNowDate());
+ return goodsMessagesMapper.insertGoodsMessages(goodsMessages);
+ }
+
+ /**
+ * 修改商品信息
+ *
+ * @param goodsMessages 商品信息
+ * @return 结果
+ */
+ @Override
+ public int updateGoodsMessages(GoodsMessages goodsMessages)
+ {
+ goodsMessages.setUpdateTime(DateUtils.getNowDate());
+ return goodsMessagesMapper.updateGoodsMessages(goodsMessages);
+ }
+
+ /**
+ * 批量删除商品信息
+ *
+ * @param ids 需要删除的商品信息主键
+ * @return 结果
+ */
+ @Override
+ public int deleteGoodsMessagesByIds(Long[] ids)
+ {
+ return goodsMessagesMapper.deleteGoodsMessagesByIds(ids);
+ }
+
+ /**
+ * 删除商品信息信息
+ *
+ * @param id 商品信息主键
+ * @return 结果
+ */
+ @Override
+ public int deleteGoodsMessagesById(Long id)
+ {
+ return goodsMessagesMapper.deleteGoodsMessagesById(id);
+ }
+
+ @Override
+ public GoodsMessages selectGoodsMessagesByCode(String goodsCode) {
+ if (StringUtils.hasText(goodsCode)){
+ return goodsMessagesMapper.selectGoodsMessagesByCode(goodsCode);
+ }
+ return null;
+ }
+}
diff --git a/sf-service/src/main/resources/mapper/service/GoodsMessagesMapper.xml b/sf-service/src/main/resources/mapper/service/GoodsMessagesMapper.xml
new file mode 100644
index 0000000..d3037a7
--- /dev/null
+++ b/sf-service/src/main/resources/mapper/service/GoodsMessagesMapper.xml
@@ -0,0 +1,134 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ select id, app_code, goods_code, stock_id, review_status, product_title, product_picture, original_price, product_desc, goods_type, goods_spec, order_num, is_delete, created, modified, create_time, update_time, goods_name, goods_model from GOODS_MESSAGES
+
+
+
+
+
+
+
+
+ insert into GOODS_MESSAGES
+
+ app_code,
+ goods_code,
+ stock_id,
+ review_status,
+ product_title,
+ product_picture,
+ original_price,
+ product_desc,
+ goods_type,
+ goods_spec,
+ order_num,
+ is_delete,
+ created,
+ modified,
+ create_time,
+ update_time,
+ goods_name,
+ goods_model,
+
+
+ #{appCode},
+ #{goodsCode},
+ #{stockId},
+ #{reviewStatus},
+ #{productTitle},
+ #{productPicture},
+ #{originalPrice},
+ #{productDesc},
+ #{goodsType},
+ #{goodsSpec},
+ #{orderNum},
+ #{isDelete},
+ #{created},
+ #{modified},
+ #{createTime},
+ #{updateTime},
+ #{goodsName},
+ #{goodsModel},
+
+
+
+
+ update GOODS_MESSAGES
+
+ goods_code = #{goodsCode},
+ stock_id = #{stockId},
+ review_status = #{reviewStatus},
+ product_title = #{productTitle},
+ product_picture = #{productPicture},
+ original_price = #{originalPrice},
+ product_desc = #{productDesc},
+ goods_type = #{goodsType},
+ goods_spec = #{goodsSpec},
+ order_num = #{orderNum},
+ is_delete = #{isDelete},
+ created = #{created},
+ modified = #{modified},
+ create_time = #{createTime},
+ update_time = #{updateTime},
+ goods_name = #{goodsName},
+ goods_model = #{goodsModel},
+
+ where id = #{id}
+
+
+
+ delete from GOODS_MESSAGES where id = #{id}
+
+
+
+ delete from GOODS_MESSAGES where id in
+
+ #{id}
+
+
+
\ No newline at end of file
diff --git a/sf-system/pom.xml b/sf-system/pom.xml
index 1a80595..d7d76c6 100644
--- a/sf-system/pom.xml
+++ b/sf-system/pom.xml
@@ -22,6 +22,10 @@
com.smarterFramework
sf-common