從0開始 獨立完成企業級Java電商網站開發(服務端)學習筆記(截止11章)

https://blog.csdn.net/w8253497062015/article/details/88382183#MBGgeneratorConfigxml_104

 

慕課網後端學習筆記
1.數據表結構設計
建表
1.unique這邊不是很懂。
2.comment這邊不是很懂。
3.password這邊之後要用MD5進行加密。
4.xxx_time後面會講解時間戳
1.parent_id是因爲分類採用樹狀分類,遞歸需要邊界條件。父類別id=0時,說明是根節點,一級類別,此時爲return條件。
2.status可選爲1或2,1表示類別正常,2表示該類別已廢棄。
1.category_id將來在表關係中要採用外鏈。
2.text格式可以存放的內容比varchar更大,存放圖片地址,採用json格式,用於拓展。
3.price採用decimal(20,2)說明價格整體最多20位(其中有兩位是小數)。後面會學習如何解決丟失精度的問題。
4.status爲商品狀態,1-在售,2-下架,3-刪除。
表關係
1.爲什麼不用外鍵?
2.爲什麼要採用唯一索引unique key
3.查業務問題的後悔藥——
2.項目初始化
Tips:
項目結構
MBG逆向工程->generatorConfig.xml
Mybatis Plugin無法破解使用(2018.3.3)
Mybatis PageHelper
logback配置
ResultMap,標籤爲constructor,自動構建爲pojo
3.用戶模塊開發
3.1值得注意的點
1.橫向越權、縱向越權安全漏洞
2.MD5明文加密及增加salt值
3.Guava緩存的使用
4.高複用服務響應對象的設計思想和抽象封裝
5.Mybatis Plugin的使用技巧
6.Session的使用
7.方法的局部演進
8.前臺用戶接口設計
9.代碼實現
4.分類管理模塊
1.如何設計及封裝無線層級的樹狀數據結構
2.遞歸算法的設計思想
3.如何處理複雜對象排重
4.重寫hashcode和equal的注意事項
5.代碼實現
5.商品模塊
前臺功能:
後臺功能:
學習目標
1.FTP服務的對接
2.SpringMVC文件上傳
3.流讀取Properties配置文件
4.POJO、BO、VO對象之間的轉換關係及解決思路
5.joda-time快速入門
6.靜態塊
7.Mybatis-PageHepler高效準確地分頁及動態排序
8.Mybatis對List遍歷的實現方法
9.Mybatis對where語句動態拼裝的幾個版本演進
6.購物車模塊
1.購物車模塊的設計思想
2.如何封裝一個高複用購物車核心方法
3.解決浮點型商業運算中丟失精度的問題
7.收貨地址模塊
1.SpringMVC數據綁定中的對象綁定
2.mybatis自動生成主鍵、配置和使用
3.如何避免橫向越權漏洞的鞏固
8.支付模塊
1.熟悉支付寶對接核心文檔,調通支付寶支付功能官方Demo
2.解析支付寶SDK對接源碼
3.RSA1和RSA2驗證簽名及加解密
4.避免支付寶重複通知和數據校驗
5.nataapp外網穿透和tomcat remote debug
6.生成二維碼,並持久化到圖片服務器
8.訂單模塊
1.避免業務邏輯中橫向越權和縱向越權等安全漏洞
2.設計使用、安全、擴展性強大的常量、枚舉類
3.訂單號生成規則、訂單嚴謹性判斷
4.POJO和VO之間的實際操練
5.mybatis批量插入
1.數據表結構設計
建表
create table mmall_user(
id int(11) PRIMARY key not null auto_increment comment '用戶表id',
username varchar(50) not null comment '用戶名',
password varchar(50) not null,
email varchar(50) DEFAULT null,
phone varchar(50) DEFAULT null,
question VARCHAR(100) DEFAULT null,
answer varchar(100) DEFAULT null,
role int(4) not null,
create_time datetime not null,
unique key user_name_unique (username) using btree
)engine=INNODB auto_increment=21 DEFAULT charset=utf8
1
2
3
4
5
6
7
8
9
10
11
12
1.unique這邊不是很懂。
答:用戶名是不能重複的。在併發的時候可以通過鎖的形式解決,但是當架構變成分佈式後,通過數據庫底層的unique key唯一索引,交給mysql完成了唯一的驗證。btree提高查詢效率。

所有的MySQL索引(PRIMARY、UNIQUE和INDEX)在B樹中存儲

2.comment這邊不是很懂。
註釋。

3.password這邊之後要用MD5進行加密。
4.xxx_time後面會講解時間戳
create table mmall_category(
id int(11) PRIMARY key auto_increment,
parent_id int(11),
status tinyint(1) default 1
)
1
2
3
4
5
1.parent_id是因爲分類採用樹狀分類,遞歸需要邊界條件。父類別id=0時,說明是根節點,一級類別,此時爲return條件。
2.status可選爲1或2,1表示類別正常,2表示該類別已廢棄。
create table mmall_product(
    id int(11) primary key auto_increment,
    category_id int(11),
    sub_images text,
    price decimal(20,2),
    status int(6) DEFAULT 1
)
1
2
3
4
5
6
7
1.category_id將來在表關係中要採用外鏈。
2.text格式可以存放的內容比varchar更大,存放圖片地址,採用json格式,用於拓展。
3.price採用decimal(20,2)說明價格整體最多20位(其中有兩位是小數)。後面會學習如何解決丟失精度的問題。
4.status爲商品狀態,1-在售,2-下架,3-刪除。
表關係


1.爲什麼不用外鍵?
回答:分庫分表有外鍵會非常麻煩,清洗數據也很麻煩。數據庫內置觸發器也不適合採用。

2.爲什麼要採用唯一索引unique key
回答:加快查詢速度

3.查業務問題的後悔藥——
時間戳

create_time 數據創建時間    
update_time 數據更新時間

可以用於查詢業務,主要要存儲datetime類型。

2.項目初始化
Tips:
<bean name="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="com.mmall.dao"/>
</bean>
1
2
3
這樣才能將dao中的Mapper類注入到IOC容器中。

項目結構
– controller

– dao

– service

– util

– vo(Value Object)

– pojo(Plain Ordinary Java Object)

common 常量/全局異常的公共類

關係:

1.DB->dao->service->controller

2.pojo是簡單的數據庫實體類,vo封裝pojo傳給前端進行展示。也可以pojo->bo(bussiness object)->vo->前端展示

MBG逆向工程->generatorConfig.xml
<!--注意,下面的那個dtd網絡好像連不上,所以需要下載下來放到本地文件中。可以去csdn下載,反正百度一下就有了-->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
    PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
    "D:/Sonihr_Soft/JavaProject/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
<!--導入屬性配置-->
<properties resource="datasource.properties"></properties>

<!--指定特定數據庫的jdbc驅動jar包的位置
這邊我本以爲在Maven中添加依賴就不用填寫了,但是經過測試這裏必須填寫。-->
<classPathEntry location="${db.driverLocation}"/>

<context id="default" targetRuntime="MyBatis3">

    <!-- optional,旨在創建class時,對註釋進行控制 -->
    <commentGenerator>
        <property name="suppressDate" value="true"/>
        <property name=" " value="true"/>
    </commentGenerator>

    <!--jdbc的數據庫連接 -->
    <jdbcConnection
            driverClass="${db.driver}"
            connectionURL="${db.url}"
            userId="${db.username}"
            password="${db.password}">
    </jdbcConnection>

    <!-- 非必需,類型處理器,在數據庫類型和java類型之間的轉換控制-->
    <javaTypeResolver>
        <!--是否強制轉換-->
        <property name="forceBigDecimals" value="false"/>
    </javaTypeResolver>

    <!-- Model模型生成器,用來生成含有主鍵key的類,記錄類 以及查詢Example類
        targetPackage     指定生成的model生成所在的包名
        targetProject     指定在該項目下所在的路徑
    -->
    <!--<javaModelGenerator targetPackage="com.mmall.pojo" targetProject=".\src\main\java">-->
    <javaModelGenerator targetPackage="sonihr.pojo" targetProject="./src/main/java">
        <!-- 是否允許子包,即targetPackage.schemaName.tableName -->
        <property name="enableSubPackages" value="false"/>
        <!-- 是否對model添加 構造函數 -->
        <property name="constructorBased" value="true"/>
        <!-- 是否對類CHAR類型的列的數據進行trim操作 -->
        <property name="trimStrings" value="true"/>
        <!-- 建立的Model對象是否 不可改變  即生成的Model對象不會有 setter方法,只有構造方法 -->
        <property name="immutable" value="false"/>
    </javaModelGenerator>

    <!--mapper映射文件生成所在的目錄 爲每一個數據庫的表生成對應的SqlMap文件 -->
    <!--<sqlMapGenerator targetPackage="mappers" targetProject=".\src\main\resources">-->
    <sqlMapGenerator targetPackage="mappers" targetProject="./src/main/resources">
        <property name="enableSubPackages" value="false"/>
    </sqlMapGenerator>

    <!-- 客戶端代碼,生成易於使用的針對Model對象和XML配置文件 的代碼
            type="ANNOTATEDMAPPER",生成Java Model 和基於註解的Mapper對象
            type="MIXEDMAPPER",生成基於註解的Java Model 和相應的Mapper對象
            type="XMLMAPPER",生成SQLMap XML文件和獨立的Mapper接口
    -->

    <!-- targetPackage:mapper接口dao生成的位置 -->
    <!--<javaClientGenerator type="XMLMAPPER" targetPackage="com.mmall.dao" targetProject=".\src\main\java">-->
    <javaClientGenerator type="XMLMAPPER" targetPackage="com.mmall.dao" targetProject="./src/main/java">
        <!-- enableSubPackages:是否讓schema作爲包的後綴 -->
        <property name="enableSubPackages" value="false" />
    </javaClientGenerator>


    <table tableName="mmall_shipping" domainObjectName="Shipping" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false"></table>
    <table tableName="mmall_cart" domainObjectName="Cart" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false"></table>
    <table tableName="mmall_cart_item" domainObjectName="CartItem" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false"></table>
    <table tableName="mmall_category" domainObjectName="Category" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false"></table>
    <table tableName="mmall_order" domainObjectName="Order" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false"></table>
    <table tableName="mmall_order_item" domainObjectName="OrderItem" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false"></table>
    <table tableName="mmall_pay_info" domainObjectName="PayInfo" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false"></table>
    <table tableName="mmall_product" domainObjectName="Product" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false">
        <columnOverride column="detail" jdbcType="VARCHAR" />
        <columnOverride column="sub_images" jdbcType="VARCHAR" />
    </table>
    <table tableName="mmall_user" domainObjectName="User" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false"></table>


    <!-- geelynote mybatis插件的搭建 -->
</context>
</generatorConfiguration>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
Mybatis Plugin無法破解使用(2018.3.3)
解決方法:採用free mybatis plugin

Mybatis PageHelper
在applicationContext-datasource.xml中,即在Spring的配置文件中配置。當然也可以在mybatis的配置文件裏配置,具體可見pagehelper的github文檔,寫的非常詳細。

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="mapperLocations" value="classpath*:mappers/*Mapper.xml"></property>
    <property name="plugins">
        <array>
            <bean class="com.github.pagehelper.PageInterceptor">
                <property name="properties">
                    <!--使用下面的方式配置參數,一行配置一個 -->
                    <value>
                        helperDialect=mysql
                    </value>
                </property>
            </bean>
        </array>
    </property>
</bean>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
logback配置
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">

<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
    <encoding>UTF-8</encoding>
    <encoder>
        <pattern>[%d{HH:mm:ss.SSS}][%p][%c{40}][%t] %m%n</pattern>
    </encoder>
    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
        <!--大於DEBUG的級別會提示-->
        <level>DEBUG</level>
    </filter>
</appender>

<!--這個的特點就是會把一定時間(maxHistory)之前的日誌異步壓縮。-->
<!--配置項目日誌-->
<appender name="mmall" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <!--<File>d:/mmalllog/mmall.log</File>-->
    <File>/developer/logs/mmall.log</File>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <fileNamePattern>/developer/logs/mmall.log.%d{yyyy-MM-dd}.gz</fileNamePattern>
        <append>true</append>
        <maxHistory>10</maxHistory>
    </rollingPolicy>
    <encoder>
        <pattern>[%d{HH:mm:ss.SSS}][%p][%c{40}][%t] %m%n</pattern>
    </encoder>
</appender>


<appender name="error" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <!--<File>d:/mmalllog/error.log</File>-->
    <File>/developer/logs/error.log</File>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <fileNamePattern>/devsoft/logs/error.log.%d{yyyy-MM-dd}.gz</fileNamePattern>
        <!--<fileNamePattern>d:/mmalllog/error.log.%d{yyyy-MM-dd}.gz</fileNamePattern>-->
        <append>true</append>
        <maxHistory>10</maxHistory>
    </rollingPolicy>
    <encoder>
        <pattern>[%d{HH:mm:ss.SSS}][%p][%c{40}][%t] %m%n</pattern>
    </encoder>
    <!--filter是級別過濾器,如果是error就接收,不然就拒絕。-->
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
        <level>ERROR</level>
        <onMatch>ACCEPT</onMatch>
        <onMismatch>DENY</onMismatch>
    </filter>
</appender>

<!--logger的name表示受到這個xml約束的某一個包或者某一個類-->
<!--appender意爲附加器,即這個logger中需要加入哪些條件-->
<logger name="com.sonihr" additivity="false" level="INFO" >
    <appender-ref ref="mmall" />
    <appender-ref ref="console"/>
</logger>

<!-- geelynote mybatis log 日誌 -->

<logger name="com.mmall.dao" level="DEBUG"/>

<!--<logger name="com.ibatis.sqlmap.engine.impl.SqlMapClientDelegate" level="DEBUG" >-->
<!--<appender-ref ref="console"/>-->
<!--</logger>-->

<!--<logger name="java.sql.Connection" level="DEBUG">-->
<!--<appender-ref ref="console"/>-->
<!--</logger>-->
<!--<logger name="java.sql.Statement" level="DEBUG">-->
<!--<appender-ref ref="console"/>-->
<!--</logger>-->

<!--<logger name="java.sql.PreparedStatement" level="DEBUG">-->
<!--<appender-ref ref="console"/>-->
<!--</logger>-->

<!--子節點<root>:它也是<loger>元素,但是它是根loger,
是所有<loger>的上級。只有一個level屬性,
因爲name已經被命名爲"root",且已經是最上級了。-->
<root level="DEBUG">
    <appender-ref ref="console"/>
    <appender-ref ref="error"/>
</root>

</configuration>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
在logback使用的時候,除了配置文件還要注意,需要在pom.xml中正確配置

ResultMap,標籤爲constructor,自動構建爲pojo
3.用戶模塊開發
<properties>
    ...
    <logback.version>1.0.11</logback.version>
    <slf4j.api.version>1.7.5</slf4j.api.version>
</properties>
<dependencies>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>${slf4j.api.version}</version>
    </dependency>
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>${logback.version}</version>
    </dependency>
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-core</artifactId>
      <version>${logback.version}</version>
    </dependency>
</dependencies>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
3.1值得注意的點
1.橫向越權、縱向越權安全漏洞
橫向越權:攻擊者嘗試訪問與他擁有相同權限的用戶的資源。

解決方式:

採用Token處理。Q:如果沒有Token?A:那麼只要有用戶名和新密碼即可進入修改密碼界面,不安全。有了Token說明已經經過了安全問題那一關。Token是UUID,key是token_username。這樣一個username就對應着一個唯一的token,爲了避免自己的token可以修改辨認的密碼。

登陸中修改密碼時候,查詢數據庫中是否存在改密碼的時候,一定要檢測這個用戶名和密碼是否是匹配的。設想這樣一種場景,我登錄這一臺機器,臨時走開,另一個人在線要修改我的密碼,但是他不知道我的密碼。這個時候他輸入了自己的密碼,然後因爲沒有檢測密碼是否匹配用戶名,結果直接把我的密碼修改了!這也太真實了8!

修改信息的時候。爲防止橫向越權,修改時綁定當前用戶的id。

縱向越權:低級別攻擊者嘗試訪問高級別用戶的資源。

2.MD5明文加密及增加salt值
對密碼進行MD5編碼,並且對原密碼增添salt值。因爲MD5碼是不可逆的,因此,即使數據庫泄露也不會導致密碼泄露。增加salt值是爲了防止與現有的庫重合,增加了破解難度。

private static String byteArrayToHexString(byte b[]) {
    StringBuffer resultSb = new StringBuffer();
    for (int i = 0; i < b.length; i++)
        resultSb.append(byteToHexString(b[i]));

    return resultSb.toString();
}

private static String byteToHexString(byte b) {
    int n = b;
    if (n < 0)
        n += 256;
    int d1 = n / 16;
    int d2 = n % 16;
    return hexDigits[d1] + hexDigits[d2];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
3.Guava緩存的使用
private static LoadingCache<String,String> localCache =
        CacheBuilder.newBuilder().initialCapacity(1000).maximumSize(10000)
                .expireAfterAccess(12, TimeUnit.HOURS)//給定時間內沒有被讀/寫訪問,則回收。
            .build(new CacheLoader<String, String>() {
            @Override
            /** 當本地緩存命沒有中時,調用load方法獲取結果並將結果緩存 **/
            public String load(String s) throws Exception {
                return "null";
            }
        });
public static void setkey(String key,String value){
    localCache.put(key,value);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
本質上是用TokenCache類封裝了一個LoadingChache<String,String>的Cache對象。這個對象是類似map的,有一組鍵值對,且內部通過LRU算法維持。可以理解成是某種在內存中常住的緩存,具有某種緩存算法進行清除管理。

4.高複用服務響應對象的設計思想和抽象封裝
package com.sonihr.common;
/*
@author 黃大寧Rhinos
@date 2019/3/2 - 14:20
**/

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.jsonschema.JsonSerializableSchema;

import javax.validation.constraints.NotNull;
import java.io.Serializable;

//因爲響應對象返回的屬性data是一個泛型,可能是pojo裏的任意對象
//保證序列化json的時候,如果是null的對象,key也會消失
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ServerResponse<T> implements Serializable {
    private int status;
    private String msg;
    private T data;

    private ServerResponse(int status, String msg, T data) {
        this.status = status;
        this.msg = msg;
        this.data = data;
    }

    private ServerResponse(int status, String msg) {
        this.status = status;
        this.msg = msg;
    }

    private ServerResponse(int status, T data) {
        this.status = status;
        this.data = data;
    }

    private ServerResponse(int status) {
        this.status = status;
    }

    private ServerResponse() {
    }
    @JsonIgnore
    //    使之不在json序列化結果當中
    public boolean isSuccess(){
        return this.status == ResponseCode.SUCCESS.getCode();
    }

    public int getStatus() {
        return status;
    }

    public String getMsg() {
        return msg;
    }

    public T getData() {
        return data;
    }

    public static <T> ServerResponse<T> createBySuccess(){
        return new ServerResponse<T>(ResponseCode.SUCCESS.getCode());
    }

    //    方法名有message的是String,沒有的是T
    public static <T> ServerResponse<T> createBySuccessMessage(String msg){
        return new ServerResponse<T>(ResponseCode.SUCCESS.getCode(),msg);
    }

    public static <T> ServerResponse<T> createBySuccess(T data){
        return new ServerResponse<T>(ResponseCode.SUCCESS.getCode(),data);
    }

    public static <T> ServerResponse<T> createBySuccess(String msg,T data){
        return new ServerResponse<T>(ResponseCode.SUCCESS.getCode(),msg,data);
    }

    public static <T> ServerResponse<T> createByError(){
        return new ServerResponse<T>(ResponseCode.ERROR.getCode(),ResponseCode.ERROR.getDesc());
    }
    public static <T> ServerResponse<T> createByErrorMessage(String errorMessage){
        return new ServerResponse<T>(ResponseCode.ERROR.getCode(),errorMessage );
    }

    public static <T> ServerResponse<T> createByErrorCodeMessage(int errorCode,String errorMessage){
        return new ServerResponse<T>(errorCode,errorMessage );
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
第一個問題:爲什麼要實現Serializable。因爲傳遞給前端的數據要通過@ResponseBody註解轉化爲JSON數據,其中要進行序列化。
第二個問題:這個是什麼作用?這個泛型類規定了一個返回值的格式,即一個返回值只能包含以下三種內容:

status 必須
msg 幾乎必須
data 有就有,沒有就沒有
之所以是泛型類,泛型的點就在於data的類型。簡而言是,你想給前端發什麼對象,這裏就要用什麼類型。所有的構造函數的都是私有的,是爲了在後面用共有類方法封裝這些構造函數,從而返回一個實例。這樣做的好處是1.類似於一個工具類,不用實例化對象出來。2.工廠模式,可以根據要求生產出需求的實例化對象。3.通過公開類方法的命名區別來避免了泛型T與String的二義性問題。(實際上沒有二義性問題,優先匹配Spring。但是當T爲String的時候,我們又想優先匹配T的時候,我們就可以在名稱中是否包含Message來判斷。)

5.Mybatis Plugin的使用技巧
會有bug,這裏採用的是Mybatis Plus,具有一樣的跳轉功能。

6.Session的使用
HttpSession,這裏主要的作用是將currentuser的所有信息保存在域對象中。

7.方法的局部演進
8.前臺用戶接口設計
接口地址:

https://gitee.com/imooccode/happymmallwiki/wikis/門戶_用戶接口?sort_id=9917

9.代碼實現
UserServiceImpl

login
register
checkValid
selectQuestion
checkAnswer
forgetResetPassword
resetPassword
updateInformation
getInformation
userMapper
UserController

login
logout
register
checkValid
getUserInfo
forgetGetQuestion
forgetCheckAnswer
forgetRestPassword
resetPassword
updateInformation
get_information
iUserService
4.分類管理模塊
1.如何設計及封裝無線層級的樹狀數據結構
設計category表的時候,包含字段id和parent_id。其本質類似於數的數據結構。遞歸的時候可以從當前分類向上找父節點,我預測這個樹是一個倒置的樹。(預測錯誤,parent_id並不是作爲邊界條,具體的預測條件下面說。)

2.遞歸算法的設計思想
典型的DFS遞歸算法。每一個id都包含n個child_id,那相當於是一個容量爲n的list。首先,這個list我們可以通過方法寫出,假設這個方法叫selectCategoryAndChildrenById。

public ServerResponse<List<Integer>> selectCategoryAndChildrenById(Integer categoryId){
    Set<Category> categorySet = Sets.newHashSet();
    findChildCategory(categorySet,categoryId);


    List<Integer> categoryIdList = Lists.newArrayList();
    if(categoryId != null){
        for(Category categoryItem : categorySet){
            categoryIdList.add(categoryItem.getId());
        }
    }
    return ServerResponse.createBySuccess(categoryIdList);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
可以看出,採用這個方法可以得到當前id的的child_id的list。然後對這組list中的每一個再執行假設這個方法叫selectCategoryAndChildrenById方法,就可以得到他們每個的child_id。
所以有了下面的算法:

//遞歸算法,算出子節點
private Set<Category> findChildCategory(Set<Category> categorySet ,Integer categoryId){
    Category category = categoryMapper.selectByPrimaryKey(categoryId);
    if(category != null){
        categorySet.add(category);
    }
    //查找子節點,遞歸算法一定要有一個退出的條件
    List<Category> categoryList = categoryMapper.selectCategoryChildrenByParentId(categoryId);
    for(Category categoryItem : categoryList){
        findChildCategory(categorySet,categoryItem.getId());
    }
    return categorySet;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
其邊界條件是,當list爲空的時候,說明是葉子節點,及return。用Set的目的是爲了去重。

3.如何處理複雜對象排重
重寫hashcode和equals方法,利用set集合。

4.重寫hashcode和equal的注意事項
兩個最好重寫,並且兩者採用的屬性相同。比如hashcode計算時用id和username,那相應的equals的時候也要比較id和username.

5.代碼實現
ICategoryService

addCategory
updateCategoryName
getChildrenParallelCategory
selectCategoryAndChildrenById
CategoryManageController

addCategory
setCategoryName
getChildrenParallelCategory
getCategoryAndDeepChildrenCategory
iUserService
iCategoryService
5.商品模塊
前臺功能:
產品搜索
動態排序列表及分頁
產品詳情
後臺功能:
商品列表
商品搜索
圖片上傳
富文本上傳
學習目標
1.FTP服務的對接
FTPClient f = new FTPClient();
f.connect(serverIP);
f.login(username, password);
1
2
3
連接ftp服務器並登陸。

private boolean uploadFile(String remotePath,List<File> fileList){
    boolean uploaded = true;
    FileInputStream fis = null;
    //連接FTP服務器
    if(connectServer(this.getIp(),this.port,this.user,this.pwd)){
        try {
            if(!ftpClient.changeWorkingDirectory(remotePath)){
                logger.info("正在創建 {} 文件夾",remotePath);
                ftpClient.makeDirectory(remotePath);
                if(!ftpClient.changeWorkingDirectory(remotePath))
                    logger.error("文件夾創建失敗");
            }
            ftpClient.setBufferSize(1024);
            ftpClient.setControlEncoding("UTF-8");
            ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);
            ftpClient.enterLocalPassiveMode();
            for(File fileItem:fileList){
                fis = new FileInputStream(fileItem);
                //通過指定的流將文件上傳至服務器
                ftpClient.storeFile(fileItem.getName(),fis);
            }
        } catch (IOException e) {
            logger.error("上傳文件異常");
            uploaded = false;
        }finally {
            try {
                fis.close();
                ftpClient.disconnect();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    return uploaded;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
主要是FtpClient這個類。

2.SpringMVC文件上傳
dispatcher-servlet.xml(SpringMVC的配置文件)

<!-- 文件上傳 -->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    <property name="maxUploadSize" value="10485760"/> <!-- 10m -->
    <property name="maxInMemorySize" value="4096" />
    <property name="defaultEncoding" value="UTF-8"></property>
</bean>
1
2
3
4
5
6
前臺jsp 注意enctype=“mutipart/form-data”

<form action="/manage/product/upload.do" name="form1" method="post" enctype="multipart/form-data">
    <input type="file" name="upload_file">
    <input type="submit" value="springMVC上傳文件"></input>
</form>
1
2
3
4
enctype就是encodetype翻譯成中文就是編碼類型的意思!multipart/form-data是指表單數據有多部分構成:既有文本數據,又有文件等二進制數據的意思。
另外需要注意的是:默認情況下,enctype的值是application/x-www-form-urlencoded,不能用於文件上傳;只有使用了multipart/form-data,才能完整的傳遞文件數據。
對於文件上傳工作,其實是在前端完成的,即,在php,java等語言處理之前,文件其實就已經被上傳到服務器了,服務器腳本語言的作用不過是將這些臨時文件持久化而已!

public String upload(MultipartFile file,String path){
    String fileName = file.getOriginalFilename();
    //擴展名
    String fileExtensionName = fileName.substring(fileName.lastIndexOf(".")+1);
    String uploadFileName = UUID.randomUUID().toString()+"."+fileExtensionName;
    logger.info("開始上傳文件,上傳文件的文件名:{},上傳的路徑:{},新文件名:{}",fileName,path,uploadFileName);
    //傳進來的path不一定存在,如果不存在,就創建一個
    File fileDir = new File(path);
    if(!fileDir.exists()){
        fileDir.setWritable(true);//賦予可寫的權限
        fileDir.mkdirs();
    }
    File targetFile = new File(path,uploadFileName);
    try {
        file.transferTo(targetFile);//文件已經上傳至web服務器
        // TODO: 2019/3/4 將targetFile上傳到我們的ftp服務器
        FTPUtil.uploadFile(Lists.<File>newArrayList(targetFile));
        // TODO: 2019/3/4 上傳完之後,刪除upload下面的文件
        targetFile.delete();
    } catch (IOException e) {
        logger.error("上傳文件異常",e);
        return null;
    }
    return targetFile.getName();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
傳進來的path爲當前webapp所在目錄的ROOT文件夾下,即WEB-INF的同級文件夾upload下。

MultipartFile類常用的方法:void transferTo(File dest) //保存到一個目標文件中。

核心代碼:

public void upload(MultipartFile file) {  
    String path = request.getSession().getServletContext().getRealPath("upload") ; //獲得路徑
    String uploadFileName = file.getOriginalFilename();//獲得項目名稱
    /*File(String parent, String child) 
    從父路徑名字符串和子路徑名字符串創建新的 File實例。*/
    File targetFile = new File(path,uploadFileName);//創建文件,路徑+文件名,比如D:/adc/adad/11.jpg
    file.transferTo(targetFile);//上傳到這裏去
}
1
2
3
4
5
6
7
8
3.流讀取Properties配置文件
private static Properties props;

static {
    String fileName = "mmall.properties";
    props = new Properties();
    try {
        props.load(new InputStreamReader(PropertiesUtil.class.getClassLoader().getResourceAsStream(fileName),"UTF-8"));
    } catch (IOException e) {
        logger.error("配置文件讀取異常",e);
    }
}

public static String getProperty(String key,String defaultValue){

    String value = props.getProperty(key.trim());
    if(StringUtils.isBlank(value)){
        value = defaultValue;
    }
    return value.trim();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
有幾個注意點:

採用PropertiesUtil.class只是爲了獲取類加載器,從而調用getResourceAsStream方法。
採用load(InputStream)的方式獲得properties中的值,並且發現put中是synchronized的。
採用getProperty方式獲取value,注意要用trim以避免空格的出現。
採用方法重載的方式,設定default值。
4.POJO、BO、VO對象之間的轉換關係及解決思路
POJO Plain Ordinary Java Object(DAO層)–封裝–>BO Business View(Servoce層)–封裝–>VO View Object(Controller層)

POJO–assemble方法封裝->Value Object(Controller和Service都用的,相當於把這兩層對的數據結構模糊了)

private ProductDetailVo assembleProductDetailVo(Product product){
    ProductDetailVo productDetailVo = new ProductDetailVo();
    productDetailVo.setId(product.getId());
    productDetailVo.setSubtitle(product.getSubtitle());
    productDetailVo.setPrice(product.getPrice());
    productDetailVo.setMainImage(product.getMainImage());
    productDetailVo.setSubImages(product.getSubImages());
    productDetailVo.setCategoryId(product.getCategoryId());
    productDetailVo.setDetail(product.getDetail());
    productDetailVo.setName(product.getName());
    productDetailVo.setStatus(product.getStatus());
    productDetailVo.setStock(product.getStock());

    //imageHost
    productDetailVo.setImageHost(PropertiesUtil.getProperty("ftp.server.http.prefix","http://img.happymall.com/"));
    //parentCategoryId
    Category category = categoryMapper.selectByPrimaryKey(product.getCategoryId());
    if(category == null){
        productDetailVo.setParentCategoryId(0);
    }else{
        productDetailVo.setParentCategoryId(category.getParentId());
    }
    //createTime
    productDetailVo.setCreateTime(DateTimeUtil.date2Str(product.getCreateTime()));
    //updateTime
    productDetailVo.setUpdateTime(DateTimeUtil.date2Str(product.getUpdateTime()));
    return productDetailVo;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
5.joda-time快速入門
public static Date str2Date(String dateTimeStr,String formatStr){
    DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern(formatStr);
    DateTime dateTime = dateTimeFormatter.parseDateTime(dateTimeStr);
    return dateTime.toDate();
}

//核心代碼:new DateTime(date).toString(STANDARD_FORMAT)
public static String date2Str(Date date,String formatStr){
    if(date==null){
        return StringUtils.EMPTY;
    }
    DateTime dateTime = new DateTime(date);
    return dateTime.toString(formatStr);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
相同的功能其實SimpleDateFormat類也可以完成,但是問題在於,SimpleDateFormat是線程不安全的,而joda-time是公認的簡單,使用且線程安全的包。
str2Date核心代碼:
DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern(STANDARD_FORMAT);
DateTime dateTime = dateTimeFormatter.parseDateTime(“2010-01-01 11:11:12”);
date2Str核心代碼:
new DateTime(date).toString(STANDARD_FORMAT)

6.靜態塊
static {
    String fileName = "mmall.properties";
    props = new Properties();
    try {
        props.load(new InputStreamReader(PropertiesUtil.class.getClassLoader().getResourceAsStream(fileName),"UTF-8"));
    } catch (IOException e) {
        logger.error("配置文件讀取異常",e);
    }
}
1
2
3
4
5
6
7
8
9
static{} 早於 {} 早於 構造函數快

當我們調用PropertiesUtil的時候,類加載器會調用static{},從而運行塊中語句,讀取mmall.properties,且只會初始化一次。之後再讀取的時候,不會運行塊中代碼,而是通過getProperty直接獲取。

7.Mybatis-PageHepler高效準確地分頁及動態排序
public ServerResponse<PageInfo> getProductList(int pageNum,int pageSize){
/*        startPage--start
    填充自己的sql查詢邏輯
    pageHelper-首尾*/
//        只有緊跟在PageHelper.startPage方法後的第一個Mybatis的查詢(Select)方法會被分頁。
    PageHelper.startPage(pageNum,pageSize);
    List<Product> productList = productMapper.selectList();
    //其實這邊的productList已經是分頁之後的結果了
    List<ProductListVo> productListVoList = Lists.newArrayList();
    for(Product productItem : productList){
        ProductListVo productListVo = assembleProductListVo(productItem);
        productListVoList.add(productListVo);
    }
    //PageInfo包含了非常全面的分頁屬性
    PageInfo pageResult = new PageInfo(productList);
    pageResult.setList(productListVoList);

    return ServerResponse.createBySuccess(pageResult);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
返回前端的data並不是list嗎,而是pageInfo。這個類相當於包裝了list,在list的基礎上還提供了很多關係分頁的信息,比如是否是最後一頁,現在是第幾頁等等,必須要有這個數據才能在前端有相應的展示。事實上,Mybatis Helper使用起來非常方便,只要將PageHelper.startPage寫在查詢語句之前即可,這裏運用到了AOP。相當於是在查詢的切面之前,增加了一個功能。

8.Mybatis對List遍歷的實現方法
<if test="categoryIdList != null" >
  and category_id in
  <foreach item="item" index="index" open="(" separator="," close=")" collection="categoryIdList">
    # {item}
  </foreach>
</if>
1
2
3
4
5
6
遍歷名爲categoryList的集合,並提取出其中的item元素。通過# {}取出item的值,index表示下標,從0開始遞增。

9.Mybatis對where語句動態拼裝的幾個版本演進
<select id="selectByNameAndProductId" resultMap="BaseResultMap" parameterType="map">
SELECT
<include refid="Base_Column_List"/>
from mmall_product
<where>
  <if test="productName != null">
    and name like # {productName}
  </if>
  <if test="productId != null">
    and id = # {productId}
  </if>
</where>
</select>
1
2
3
4
5
6
7
8
9
10
11
12
13
where標籤無需考慮連接條件的and。where 標記會自動將其後第一個條件的and或者是or給忽略掉

6.購物車模塊
1.購物車模塊的設計思想
CartProductVo:其中封裝了關於商品和購物車的信息,包括購物車部分屬性(id,限制數量,是否被勾選),商品的部分屬性(id,價格,圖片,狀態)

public class CartProductVo {
    //結合了產品和購物車的一個抽象對象
    private Integer id;
    private Integer userId;
    private Integer productId;
    private Integer quantity;//購物車中此商品的數量
    private String productName;
    private String productSubtitle;
    private String productMainImage;
    private BigDecimal productPrice;
    private Integer productStatus;
    private BigDecimal productTotalPrice;
    private Integer productstock;
    private Integer productChecked;//此商品是否被勾選

    private String limitQuantity;//此商品的限制數量的返回結果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CartVo <-- List:CartVo中包括了一個List,因爲一個購物車是由多個CartProductVo構成的。相當於一種產品是在cart表中爲一個單獨記錄,查詢userId相同的可以得到該用戶的購物車中的cartproductList信息,然後再他們這些記錄封裝進CartVo中。包括總價格,是否全部勾選等信息。

public class CartVo {
    private List<CartProductVo> cartProductVoList;
    private BigDecimal cartTotalPrice;
    private Boolean allChecked;//是否都勾選
    private String imageHost;
1
2
3
4
5
2.如何封裝一個高複用購物車核心方法
private CartVo getCartVoLimit(Integer userId){
    CartVo cartVo = new CartVo();
    List<Cart> cartList = cartMapper.selectCartByUserId(userId);
    List<CartProductVo> cartProductVoList = Lists.newArrayList();

    BigDecimal cartTotalPrice = new BigDecimal("0");
    if(CollectionUtils.isNotEmpty(cartList)){
        for(Cart cartItem : cartList){
            CartProductVo cartProductVo = new CartProductVo();
            cartProductVo.setId(cartItem.getId());
            cartProductVo.setUserId(userId);
            cartProductVo.setProductId(cartItem.getProductId());

            Product product = productMapper.selectByPrimaryKey(cartItem.getProductId());
            if(product!=null){
                cartProductVo.setProductMainImage(product.getMainImage());
                cartProductVo.setProductName(product.getName());
                cartProductVo.setProductSubtitle(product.getSubtitle());
                cartProductVo.setProductStatus(product.getStatus());
                cartProductVo.setProductPrice(product.getPrice());
                cartProductVo.setProductstock(product.getStock());
                //判斷庫存
                int buyLimitCount = 0;
                if(product.getStock() >= cartItem.getQuantity()){
                    //庫存充足是
                    buyLimitCount = cartItem.getQuantity();
                    cartProductVo.setLimitQuantity(Const.Cart.LIMIT_NUM_SUCCESS);
                }else{
                    buyLimitCount = product.getStock();
                    cartProductVo.setLimitQuantity(Const.Cart.LIMIT_NUM_FAIL);
                    //購物車中更新有效庫存
                    Cart cartForQuantity = new Cart();
                    cartForQuantity.setId(cartItem.getId());
                    cartForQuantity.setQuantity(buyLimitCount);
                    cartMapper.updateByPrimaryKeySelective(cartForQuantity);
                }
                cartProductVo.setQuantity(buyLimitCount);
                //計算總價(單個商品)
                cartProductVo.setProductTotalPrice(BigDecimalUtil.mul(product.getPrice().doubleValue(),cartProductVo.getQuantity().doubleValue()));
                cartProductVo.setProductChecked(cartItem.getChecked());
            }
            if(cartItem.getChecked() == Const.Cart.CHECKED){
                //如果已經勾選,增加到購物車總價中
                cartTotalPrice = BigDecimalUtil.add(cartTotalPrice.doubleValue(),cartProductVo.getProductTotalPrice().doubleValue());
            }
            cartProductVoList.add(cartProductVo);
        }
    }
    cartVo.setCartTotalPrice(cartTotalPrice);
    cartVo.setCartProductVoList(cartProductVoList);
    cartVo.setAllChecked(getAllCheckedStatuc(userId));
    cartVo.setImageHost(PropertiesUtil.getProperty("ftp.server.http.prefix"));
    return cartVo;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
這個核心方法的邏輯其實本質上是類似一個包裝設計模式。userId可以查找出這個用戶購物車中所有的產品,這是基本的數據,即cartList。然後對cartList中的每個商品(因爲cart表中的存儲方式是一個商品佔用一條記錄),或者說每個記錄,即cartItem進行操作。首先封裝進cartprocutvo,這些是每個cartitem中需要對前端展示的,即cartproductvo是對cartItem的第一層封裝。然後就是方法名中Limit的作用,即對每個cartItem中的商品判斷是否購物車數量大於存量,如果大於存量就爲存量,否則可以採用購物車數量。以上所說對的都還是對cartItem的處理,最後就是對cart整體的處理,要把list放進去,然後存入總價,數量,是否全選等屬性即可。

3.解決浮點型商業運算中丟失精度的問題
public void add(){
    System.out.println(0.05+0.01);
    //0.060000002 丟失精度
}

@Test
public void test03(){
    BigDecimal b1 = new BigDecimal("0.05");
    BigDecimal b2 = new BigDecimal("0.01");
    System.out.println(b1.add(b2));
}
1
2
3
4
5
6
7
8
9
10
11
用BigDecimal的String構造器可以做到不丟失精度。

7.收貨地址模塊
1.SpringMVC數據綁定中的對象綁定
public ServerResponse add(HttpSession session, Shipping shipping)
1
傳入的是Shipping的屬性,springMVC會自動封裝成shipping對象,調用的是無參構造方法和getter和settetr。

2.mybatis自動生成主鍵、配置和使用
<insert id="insert" parameterType="com.mmall.pojo.Shipping" useGeneratedKeys="true" keyProperty="id">
insert into mmall_shipping (id, user_id, receiver_name, 
  receiver_phone, receiver_mobile, receiver_province, 
  receiver_city, receiver_district, receiver_address, 
  receiver_zip, create_time, update_time
  )
values (# {id,jdbcType=INTEGER}, # {userId,jdbcType=INTEGER}, # {receiverName,jdbcType=VARCHAR}, 
  # {receiverPhone,jdbcType=VARCHAR}, # {receiverMobile,jdbcType=VARCHAR}, # {receiverProvince,jdbcType=VARCHAR}, 
  # {receiverCity,jdbcType=VARCHAR}, # {receiverDistrict,jdbcType=VARCHAR}, # {receiverAddress,jdbcType=VARCHAR}, 
  # {receiverZip,jdbcType=VARCHAR}, now(), now()
  )
</insert>
1
2
3
4
5
6
7
8
9
10
11
12
注意這裏的useGeneratedKeys=“true” keyProperty=“id”,表示採用了自動生成主鍵,並且在執行:

shippingMapper.insert(shipping);
1
此時,會把id自動賦值給shipping的id屬性。

3.如何避免橫向越權漏洞的鞏固
public ServerResponse del(Integer userId, Integer shippingId){
    int rowCount = shippingMapper.deleteByShippingIdUserId(userId,shippingId);
    if(rowCount>0){
        return ServerResponse.createBySuccess("刪除地址成功");
    }
    return ServerResponse.createByErrorMessage("刪除地址失敗");
}
1
2
3
4
5
6
7
刪除地址的查詢條件需要結合userId和shippingId

public ServerResponse update(Integer userId, Shipping shipping){
    shipping.setUserId(userId);
    int rowCount = shippingMapper.updateByShipping(shipping);
    if(rowCount>0){
        return ServerResponse.createBySuccessMessage("刪除地址成功");
    }
    return ServerResponse.createByErrorMessage("刪除地址失敗");
}
1
2
3
4
5
6
7
8
只有session裏的userId纔是真的,get?userId=xx&userName=sonihr中的userId有可能是手動xjb輸入的,因此會產生橫向越權問題。

8.支付模塊
支付寶對接
支付回調
查詢支付狀態
1.熟悉支付寶對接核心文檔,調通支付寶支付功能官方Demo
一些重要的官方文檔

沙箱調試環境(買家賬號測試,商家賬號測試)

支付寶掃碼支付主業務流程

支付寶掃碼支付流程

支付寶掃碼支付重要的字段

支付寶掃碼支付重要細節

支付寶掃碼支付對接技巧

支付寶掃碼支付官方Demo調試

2.解析支付寶SDK對接源碼
public ServerResponse pay(Long orderNo,Integer userId,String path){
    Map<String,String> resultMap = Maps.newHashMap();
    Order order = orderMapper.selectByUserIdAndOrderNo(userId,orderNo);
    if(order == null){
        ServerResponse.createByErrorMessage("用戶沒有該訂單");
    }
    resultMap.put("orderNo",String.valueOf(order.getOrderNo()));

    // (必填) 商戶網站訂單系統中唯一訂單號,64個字符以內,只能包含字母、數字、下劃線,
// 需保證商戶系統端不能重複,建議通過數據庫sequence生成,
String outTradeNo = order.getOrderNo().toString();

// (必填) 訂單標題,粗略描述用戶的支付目的。如“xxx品牌xxx門店當面付掃碼消費”
String subject = new StringBuilder().append("mmall掃碼支付,訂單號:").append(outTradeNo).toString();

// (必填) 訂單總金額,單位爲元,不能超過1億元
// 如果同時傳入了【打折金額】,【不可打折金額】,【訂單總金額】三者,則必須滿足如下條件:【訂單總金額】=【打折金額】+【不可打折金額】
String totalAmount = order.getPayment().toString();

// (可選) 訂單不可打折金額,可以配合商家平臺配置折扣活動,如果酒水不參與打折,則將對應金額填寫至此字段
// 如果該值未傳入,但傳入了【訂單總金額】,【打折金額】,則該值默認爲【訂單總金額】-【打折金額】
String undiscountableAmount = "0";

// 賣家支付寶賬號ID,用於支持一個簽約賬號下支持打款到不同的收款賬號,(打款到sellerId對應的支付寶賬號)
// 如果該字段爲空,則默認爲與支付寶簽約的商戶的PID,也就是appid對應的PID
String sellerId = "";

// 訂單描述,可以對交易或商品進行一個詳細地描述,比如填寫"購買商品2件共15.00元"
String body = new StringBuilder().append("訂單號: ").append(order.getOrderNo()).append("購買商品共: ").append(totalAmount).append("元").toString();

// 商戶操作員編號,添加此參數可以爲商戶操作員做銷售統計
String operatorId = "test_operator_id";

// (必填) 商戶門店編號,通過門店號和商家後臺可以配置精準到門店的折扣信息,詳詢支付寶技術支持
String storeId = "test_store_id";

// 業務擴展參數,目前可添加由支付寶分配的系統商編號(通過setSysServiceProviderId方法),詳情請諮詢支付寶技術支持
ExtendParams extendParams = new ExtendParams();
    extendParams.setSysServiceProviderId("2088100200300400500");

// 支付超時,定義爲120分鐘
String timeoutExpress = "120m";

// 商品明細列表,需填寫購買商品詳細信息,
List<GoodsDetail> goodsDetailList = new ArrayList<GoodsDetail>();

List<OrderItem> orderItemList = orderItemMapper.getByOrderNoUserId(orderNo,userId);

    for (OrderItem o:orderItemList) {

    String price =  BigDecimalUtil.mul(o.getCurrentUnitPrice().doubleValue(),new Double(100)).toString();
    String new_price = price.substring(0,price.length()-3);
    GoodsDetail goods = GoodsDetail.newInstance(o.getProductId().toString(), o.getProductName(),
            Long.valueOf(new_price),o.getQuantity());
    goodsDetailList.add(goods);
}

// 創建掃碼支付請求builder,設置請求參數
AlipayTradePrecreateRequestBuilder builder = new AlipayTradePrecreateRequestBuilder()
        .setSubject(subject).setTotalAmount(totalAmount).setOutTradeNo(outTradeNo)
        .setUndiscountableAmount(undiscountableAmount).setSellerId(sellerId).setBody(body)
        .setOperatorId(operatorId).setStoreId(storeId).setExtendParams(extendParams)
        .setTimeoutExpress(timeoutExpress)
        .setNotifyUrl(PropertiesUtil.getProperty("alipay.callback.url"))//支付寶服務器主動通知商戶服務器裏指定的頁面http路徑,根據需要設置
        .setGoodsDetailList(goodsDetailList);

    Configs.init("zfbinfo.properties");
AlipayTradeService tradeService = new AlipayTradeServiceImpl.ClientBuilder().build();

AlipayF2FPrecreateResult result = tradeService.tradePrecreate(builder);
    switch (result.getTradeStatus()) {
    case SUCCESS:
        log.info("支付寶預下單成功: )");

        AlipayTradePrecreateResponse response = result.getResponse();
        dumpResponse(response);

        File folder = new File(path);
        if(!folder.exists()){
            folder.setWritable(true);
            folder.mkdirs();
        }

        // 需要修改爲運行機器上的路徑
        // 細節細節細節
        String qrPath = String.format(path+"/qr-%s.png",response.getOutTradeNo());
        String qrFileName = String.format("qr-%s.png",response.getOutTradeNo());
        //生成二維碼
        ZxingUtils.getQRCodeImge(response.getQrCode(),256,qrPath);

        File targetFile = new File(path,qrFileName);
        FTPUtil.uploadFile(Lists.newArrayList(targetFile));

        log.info("qrPath:" + qrPath);
        String qrUrl =PropertiesUtil.getProperty("ftp.server.http.prefix")+targetFile.getName();
        resultMap.put("qrUrl",qrUrl);
        return ServerResponse.createBySuccess(resultMap);
    case FAILED:
        log.error("支付寶預下單失敗!!!");
        return ServerResponse.createByErrorMessage("支付寶預下單失敗!!!");
    case UNKNOWN:
        log.error("系統異常,預下單狀態未知!!!");
        return ServerResponse.createByErrorMessage("系統異常,預下單狀態未知!!!");
    default:
        log.error("不支持的交易狀態,交易返回異常!!!");
        return ServerResponse.createByErrorMessage("不支持的交易狀態,交易返回異常!!!");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
3.RSA1和RSA2驗證簽名及加解密
軟件實現。

4.避免支付寶重複通知和數據校驗
public Object alipayCallback(HttpServletRequest request){
    Map<String,String> params = Maps.newHashMap();

    Map requestParams = request.getParameterMap();
    for(Iterator iter = requestParams.keySet().iterator();iter.hasNext();){
        String name =(String)iter.next();
        String[] values = (String[]) requestParams.get(name);
        String valueStr = "";
        for(int i=0;i<values.length;i++){
            valueStr = (i==values.length-1)?valueStr+values[i]:valueStr+values[i]+",";
        }
        params.put(name,valueStr);
    }
    log.info("支付寶回調,sign:{},trade_status:{},參數:{}",
            params.get("sign"),params.get("trade_status"),params.toString());

    //非常重要,驗證回調的正確性,是不是支付寶發的,還有避免重複性
    params.remove("sign_type");
    try {
        boolean alipayRSACheckedV2 = AlipaySignature.rsaCheckV2(params, Configs.getAlipayPublicKey(),"utf-8",Configs.getSignType());
        if(!alipayRSACheckedV2){
            return ServerResponse.createByErrorMessage("非法請求,驗證不通過");
        }
    } catch (AlipayApiException e) {
        log.error("支付寶驗證回調異常");
        e.printStackTrace();
    }

    ServerResponse serverResponse = iOrderService.aliCallBack(params);
    if(serverResponse.isSuccess()){
        return Const.AlipayCallBack.RESPONSE_SUCCESS;
    }
    return Const.AlipayCallBack.RESPONSE_FAILED;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
第一步:先獲取request中的所有鍵值對參數。

第二步:利用SDK中的

AlipaySignature.rsaCheckV2(params, Configs.getAlipayPublicKey(),"utf-8",Configs.getSignType());
1
進行數據校驗。

第三步:設置訂單狀態,並且設置PayInfo信息,填充PageInfo數據。

5.nataapp外網穿透和tomcat remote debug
花生殼。

6.生成二維碼,並持久化到圖片服務器
String qrPath = String.format(path+"/qr-%s.png",response.getOutTradeNo());
String qrFileName = String.format("qr-%s.png",response.getOutTradeNo());
//生成二維碼
ZxingUtils.getQRCodeImge(response.getQrCode(),256,qrPath);

File targetFile = new File(path,qrFileName);
FTPUtil.uploadFile(Lists.newArrayList(targetFile));

log.info("qrPath:" + qrPath);
1
2
3
4
5
6
7
8
9
將二維碼持久化至本地文件夾,這裏沒有用Nginx進行反向代理。注意到:

String path = request.getSession().getServletContext().getRealPath("upload");
1
獲得的地址末尾並沒有/。

8.訂單模塊
1.避免業務邏輯中橫向越權和縱向越權等安全漏洞
2.設計使用、安全、擴展性強大的常量、枚舉類
3.訂單號生成規則、訂單嚴謹性判斷
肯定不能用主鍵,不然會被競爭對手發現你一天的成交量。
分庫分表後,假如以訂單號第5個數字進行查表操作。
本期中通過訂單時間,然後取模

private long generateOrderNo(){
    long current = System.currentTimeMillis();
    return current + new Random().nextInt(100);
}
1
2
3
4
擴展可採用雪花算法 ,snowflake

4.POJO和VO之間的實際操練
5.mybatis批量插入
<insert id="batchInsert" parameterType="list">
insert into mmall_order_item (id, order_no,user_id, product_id,
  product_name, product_image, current_unit_price,
  quantity, total_price, create_time,
  update_time)
values
<foreach collection="orderItemList" index="index" item="item" separator=",">
  (
  # {item.id},# {item.orderNo},# {item.userId},# {item.productId},# {item.productName},# {item.productImage},# {item.currentUnitPrice},# {item.quantity},# {item.totalPrice},now(),now()
  )
</foreach>
</insert>
--------------------- 
作者:Sonihr 
來源:CSDN 
原文:https://blog.csdn.net/w8253497062015/article/details/88382183 
版權聲明:本文爲博主原創文章,轉載請附上博文鏈接!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章