Ver código fonte

移植轨迹还原到原图功能

luoshi 6 anos atrás
pai
commit
603d51f4ee

+ 221 - 202
stmms-biz/src/main/java/cn/com/qmth/stmms/biz/exam/model/Exam.java

@@ -1,202 +1,221 @@
-package cn.com.qmth.stmms.biz.exam.model;
-
-import java.io.Serializable;
-import java.util.Date;
-
-import javax.persistence.Column;
-import javax.persistence.Entity;
-import javax.persistence.EnumType;
-import javax.persistence.Enumerated;
-import javax.persistence.GeneratedValue;
-import javax.persistence.Id;
-import javax.persistence.Table;
-import javax.persistence.Temporal;
-import javax.persistence.TemporalType;
-
-import cn.com.qmth.stmms.common.enums.ExamStatus;
-import cn.com.qmth.stmms.common.enums.MarkMode;
-
-@Entity
-@Table(name = "eb_exam")
-public class Exam implements Serializable {
-
-    private static final long serialVersionUID = 5179623303410999209L;
-
-    public static final String SUBJECT_ID_SPLIT = ",";
-
-    @Id
-    @GeneratedValue
-    private Integer id;
-
-    private String name;
-
-    @Column(name = "school_id")
-    private Integer schoolId;
-
-    @Temporal(TemporalType.DATE)
-    @Column(name = "exam_time")
-    private Date examTime;
-
-    @Enumerated(EnumType.ORDINAL)
-    private ExamStatus status;
-
-    @Column(name = "description", length = 128, nullable = true)
-    private String description;
-
-    /**
-     * 开启强制特殊标记
-     */
-    @Column(name = "force_special_tag", nullable = false)
-    private boolean forceSpecialTag;
-
-    @Temporal(TemporalType.TIMESTAMP)
-    @Column(name = "create_time")
-    private Date createTime;
-
-    @Temporal(TemporalType.TIMESTAMP)
-    @Column(name = "update_time")
-    private Date updateTime;
-
-    @Column(name = "creator_id")
-    private Integer creatorId;
-
-    /**
-     * 开启/关闭 原卷显示
-     */
-    @Column(name = "show_sheet", nullable = false)
-    private boolean showSheet;
-
-    /**
-     * 强制评卷模式
-     */
-    @Column(name = "mark_mode", nullable = true)
-    @Enumerated(EnumType.STRING)
-    private MarkMode markMode;
-
-    /**
-     * 评卷起始时间
-     */
-    @Temporal(TemporalType.TIMESTAMP)
-    @Column(name = "mark_start_time")
-    private Date startTime;
-
-    /**
-     * 评卷结束时间
-     */
-    @Temporal(TemporalType.TIMESTAMP)
-    @Column(name = "mark_end_time")
-    private Date endTime;
-
-    public Integer getId() {
-        return id;
-    }
-
-    public void setId(Integer id) {
-        this.id = id;
-    }
-
-    public String getName() {
-        return name;
-    }
-
-    public void setName(String name) {
-        this.name = name;
-    }
-
-    public String getDescription() {
-        return description;
-    }
-
-    public void setDescription(String description) {
-        this.description = description;
-    }
-
-    public Date getExamTime() {
-        return examTime;
-    }
-
-    public void setExamTime(Date examTime) {
-        this.examTime = examTime;
-    }
-
-    public Date getCreateTime() {
-        return createTime;
-    }
-
-    public void setCreateTime(Date createTime) {
-        this.createTime = createTime;
-    }
-
-    public Integer getCreatorId() {
-        return creatorId;
-    }
-
-    public void setCreatorId(Integer creatorId) {
-        this.creatorId = creatorId;
-    }
-
-    public Date getUpdateTime() {
-        return updateTime;
-    }
-
-    public void setUpdateTime(Date updateTime) {
-        this.updateTime = updateTime;
-    }
-
-    public ExamStatus getStatus() {
-        return status;
-    }
-
-    public void setStatus(ExamStatus status) {
-        this.status = status;
-    }
-
-    public Integer getSchoolId() {
-        return schoolId;
-    }
-
-    public void setSchoolId(Integer schoolId) {
-        this.schoolId = schoolId;
-    }
-
-    public boolean isForceSpecialTag() {
-        return forceSpecialTag;
-    }
-
-    public void setForceSpecialTag(boolean forceSpecialTag) {
-        this.forceSpecialTag = forceSpecialTag;
-    }
-
-    public boolean isShowSheet() {
-        return showSheet;
-    }
-
-    public void setShowSheet(boolean showSheet) {
-        this.showSheet = showSheet;
-    }
-
-    public MarkMode getMarkMode() {
-        return markMode;
-    }
-
-    public void setMarkMode(MarkMode markMode) {
-        this.markMode = markMode;
-    }
-
-    public Date getStartTime() {
-        return startTime;
-    }
-
-    public void setStartTime(Date startTime) {
-        this.startTime = startTime;
-    }
-
-    public Date getEndTime() {
-        return endTime;
-    }
-
-    public void setEndTime(Date endTime) {
-        this.endTime = endTime;
-    }
-
-}
+package cn.com.qmth.stmms.biz.exam.model;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+import javax.persistence.Table;
+import javax.persistence.Temporal;
+import javax.persistence.TemporalType;
+
+import cn.com.qmth.stmms.biz.mark.model.PictureConfigItem;
+import cn.com.qmth.stmms.common.enums.ExamStatus;
+import cn.com.qmth.stmms.common.enums.MarkMode;
+
+@Entity
+@Table(name = "eb_exam")
+public class Exam implements Serializable {
+
+    private static final long serialVersionUID = 5179623303410999209L;
+
+    public static final String SUBJECT_ID_SPLIT = ",";
+
+    @Id
+    @GeneratedValue
+    private Integer id;
+
+    private String name;
+
+    @Column(name = "school_id")
+    private Integer schoolId;
+
+    @Temporal(TemporalType.DATE)
+    @Column(name = "exam_time")
+    private Date examTime;
+
+    @Enumerated(EnumType.ORDINAL)
+    private ExamStatus status;
+
+    @Column(name = "description", length = 128, nullable = true)
+    private String description;
+
+    /**
+     * 开启强制特殊标记
+     */
+    @Column(name = "force_special_tag", nullable = false)
+    private boolean forceSpecialTag;
+
+    @Temporal(TemporalType.TIMESTAMP)
+    @Column(name = "create_time")
+    private Date createTime;
+
+    @Temporal(TemporalType.TIMESTAMP)
+    @Column(name = "update_time")
+    private Date updateTime;
+
+    @Column(name = "creator_id")
+    private Integer creatorId;
+
+    /**
+     * 开启/关闭 原卷显示
+     */
+    @Column(name = "show_sheet", nullable = false)
+    private boolean showSheet;
+
+    /**
+     * 强制评卷模式
+     */
+    @Column(name = "mark_mode", nullable = true)
+    @Enumerated(EnumType.STRING)
+    private MarkMode markMode;
+
+    /**
+     * 评卷起始时间
+     */
+    @Temporal(TemporalType.TIMESTAMP)
+    @Column(name = "mark_start_time")
+    private Date startTime;
+
+    /**
+     * 评卷结束时间
+     */
+    @Temporal(TemporalType.TIMESTAMP)
+    @Column(name = "mark_end_time")
+    private Date endTime;
+
+    /**
+     * 裁切图配置
+     */
+    @Column(name = "slice_config", nullable = true)
+    private String sliceConfig;
+
+    public Integer getId() {
+        return id;
+    }
+
+    public void setId(Integer id) {
+        this.id = id;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    public Date getExamTime() {
+        return examTime;
+    }
+
+    public void setExamTime(Date examTime) {
+        this.examTime = examTime;
+    }
+
+    public Date getCreateTime() {
+        return createTime;
+    }
+
+    public void setCreateTime(Date createTime) {
+        this.createTime = createTime;
+    }
+
+    public Integer getCreatorId() {
+        return creatorId;
+    }
+
+    public void setCreatorId(Integer creatorId) {
+        this.creatorId = creatorId;
+    }
+
+    public Date getUpdateTime() {
+        return updateTime;
+    }
+
+    public void setUpdateTime(Date updateTime) {
+        this.updateTime = updateTime;
+    }
+
+    public ExamStatus getStatus() {
+        return status;
+    }
+
+    public void setStatus(ExamStatus status) {
+        this.status = status;
+    }
+
+    public Integer getSchoolId() {
+        return schoolId;
+    }
+
+    public void setSchoolId(Integer schoolId) {
+        this.schoolId = schoolId;
+    }
+
+    public boolean isForceSpecialTag() {
+        return forceSpecialTag;
+    }
+
+    public void setForceSpecialTag(boolean forceSpecialTag) {
+        this.forceSpecialTag = forceSpecialTag;
+    }
+
+    public boolean isShowSheet() {
+        return showSheet;
+    }
+
+    public void setShowSheet(boolean showSheet) {
+        this.showSheet = showSheet;
+    }
+
+    public MarkMode getMarkMode() {
+        return markMode;
+    }
+
+    public void setMarkMode(MarkMode markMode) {
+        this.markMode = markMode;
+    }
+
+    public Date getStartTime() {
+        return startTime;
+    }
+
+    public void setStartTime(Date startTime) {
+        this.startTime = startTime;
+    }
+
+    public Date getEndTime() {
+        return endTime;
+    }
+
+    public void setEndTime(Date endTime) {
+        this.endTime = endTime;
+    }
+
+    public String getSliceConfig() {
+        return sliceConfig;
+    }
+
+    public void setSliceConfig(String sliceConfig) {
+        this.sliceConfig = sliceConfig;
+    }
+
+    public List<PictureConfigItem> getSliceConfigList() {
+        return PictureConfigItem.parse(sliceConfig);
+    }
+}

+ 113 - 104
stmms-biz/src/main/java/cn/com/qmth/stmms/biz/exam/service/ExamStudentService.java

@@ -1,104 +1,113 @@
-package cn.com.qmth.stmms.biz.exam.service;
-
-import java.util.List;
-
-import cn.com.qmth.stmms.biz.exam.model.ExamStudent;
-import cn.com.qmth.stmms.biz.exam.query.ExamStudentSearchQuery;
-
-public interface ExamStudentService {
-
-    public ExamStudent findById(int id);
-
-    public int batchSave(List<ExamStudent> list);
-
-    public ExamStudent save(ExamStudent student);
-
-    public void deleteById(int id);
-
-    public void delete(ExamStudent student);
-
-    public void deleteByExamId(int examId);
-
-    public ExamStudentSearchQuery findByQuery(ExamStudentSearchQuery query);
-
-    public long countByQuery(final ExamStudentSearchQuery query);
-
-    List<ExamStudent> findByExamId(int examId);
-
-    List<ExamStudent> findByExamIdAndUpload(int examId, boolean upload, int pageNumber, int pageSize);
-
-    List<ExamStudent> findByExamIdAndUploadAndAbsent(int examId, boolean upload, boolean absent, int pageNumber,
-            int pageSize);
-
-    ExamStudent findByExamIdAndExamNumber(int examId, String examNumber);
-
-    public long countByExamId(int examId);
-
-    public long countByExamIdAndSubjectCode(int examId, String subjectCode);
-
-    public long countByExamIdAndSubjectCode(int examId, String subjectCode, boolean upload);
-
-    List<String> findDistinctCampusName(int examId);
-
-    ExamStudentSearchQuery findDistinctCampusName(ExamStudentSearchQuery query);
-
-    List<ExamStudent> findByExamIdAndCampusName(int examId, String campusCode, int pageNumber, int pageSize);
-
-    List<String> findDistinctPackageCode(int examId);
-
-    long countByExamIdAndCampusName(int examId, String campusName);
-
-    long countByExamIdAndCampusName(int examId, String campusName, boolean upload);
-
-    long countByExamIdAndSubjectCode(int examId, String subjectCode, boolean upload, boolean absent);
-
-    public long countByNoAbsentAndBreach(int examId, String subjectCode, boolean upload, boolean absent,
-            boolean breach);
-
-    public long countByAbsentAndBreach(int examId, String subjectCode, Boolean absent, Boolean breach);
-
-    long countCampusByExam(int examId);
-
-    long countByExamIdAndUpload(int examId, boolean upload);
-
-    void updateSubjectiveScore(int id, double score, String scoreList);
-
-    void updateManualAbsent(int id, boolean manualAbsent);
-
-    void updateManualAbsent(int examId, String examNumber, boolean manualAbsent);
-
-    void clearManualAbsent(int examId);
-
-    void updateException(int id, boolean exception);
-
-    public ExamStudent findBySchoolIdAndSubjectCodeAndStudentCode(Integer schoolId, String subjectCode,
-            String studentCode);
-
-    public ExamStudent findBySchoolIdAndSubjectCodeAndStudentCodeAndRemark(Integer schoolId, String subjectCode,
-            String studentCode, String examSeqCode);
-
-    ExamStudent findByExamIdAndSchoolIdAndSubjectCodeAndStudentCode(Integer examId, Integer schoolId,
-            String subjectCode, String studentCode);
-
-    ExamStudent findByExamIdAndSubjectCodeAndStudentCode(Integer examId, String subjectCode, String studentCode);
-
-    public List<Object[]> statisticsByExamIdAndSubjectCode(Integer examId, String code, Boolean upload, boolean absent);
-
-    public List<Object[]> statisticsByExamIdAndSubjectCode(Integer examId, String code);
-
-    public Long countByExamIdAndSubjectCodeAndCampus(Integer examId, String code, String campusName, boolean upload,
-            boolean absent);
-
-    public ExamStudent findBySchoolIdAndSubjectCodeStartingWithAndStudentCode(Integer schoolId, String subjectCode,
-            String studentCode);
-
-    public String findIdsByMarkLogin(String markLogin);
-
-    public String findIdsByMarkName(String markName);
-
-    List<ExamStudent> findByExamId(int examId, int pageNumber, int pageSize);
-
-    List<Object[]> statisticsByAbsentAndBreach(Integer examId, String code, Boolean upload, boolean absent,
-            boolean breach);
-
-}
+package cn.com.qmth.stmms.biz.exam.service;
+
+import java.util.List;
+import java.util.Map;
+
+import cn.com.qmth.stmms.biz.exam.model.ExamStudent;
+import cn.com.qmth.stmms.biz.exam.query.ExamStudentSearchQuery;
+import cn.com.qmth.stmms.biz.utils.OriginTag;
+import cn.com.qmth.stmms.biz.utils.PictureTag;
+
+public interface ExamStudentService {
+
+    public ExamStudent findById(int id);
+
+    public int batchSave(List<ExamStudent> list);
+
+    public ExamStudent save(ExamStudent student);
+
+    public void deleteById(int id);
+
+    public void delete(ExamStudent student);
+
+    public void deleteByExamId(int examId);
+
+    public ExamStudentSearchQuery findByQuery(ExamStudentSearchQuery query);
+
+    public long countByQuery(final ExamStudentSearchQuery query);
+
+    List<ExamStudent> findByExamId(int examId);
+
+    List<ExamStudent> findByExamIdAndUpload(int examId, boolean upload, int pageNumber, int pageSize);
+
+    List<ExamStudent> findByExamIdAndUploadAndAbsent(int examId, boolean upload, boolean absent, int pageNumber,
+            int pageSize);
+
+    ExamStudent findByExamIdAndExamNumber(int examId, String examNumber);
+
+    public long countByExamId(int examId);
+
+    public long countByExamIdAndSubjectCode(int examId, String subjectCode);
+
+    public long countByExamIdAndSubjectCode(int examId, String subjectCode, boolean upload);
+
+    List<String> findDistinctCampusName(int examId);
+
+    ExamStudentSearchQuery findDistinctCampusName(ExamStudentSearchQuery query);
+
+    List<ExamStudent> findByExamIdAndCampusName(int examId, String campusCode, int pageNumber, int pageSize);
+
+    List<String> findDistinctPackageCode(int examId);
+
+    long countByExamIdAndCampusName(int examId, String campusName);
+
+    long countByExamIdAndCampusName(int examId, String campusName, boolean upload);
+
+    long countByExamIdAndSubjectCode(int examId, String subjectCode, boolean upload, boolean absent);
+
+    public long countByNoAbsentAndBreach(int examId, String subjectCode, boolean upload, boolean absent,
+            boolean breach);
+
+    public long countByAbsentAndBreach(int examId, String subjectCode, Boolean absent, Boolean breach);
+
+    long countCampusByExam(int examId);
+
+    long countByExamIdAndUpload(int examId, boolean upload);
+
+    void updateSubjectiveScore(int id, double score, String scoreList);
+
+    void updateManualAbsent(int id, boolean manualAbsent);
+
+    void updateManualAbsent(int examId, String examNumber, boolean manualAbsent);
+
+    void clearManualAbsent(int examId);
+
+    void updateException(int id, boolean exception);
+
+    public ExamStudent findBySchoolIdAndSubjectCodeAndStudentCode(Integer schoolId, String subjectCode,
+            String studentCode);
+
+    public ExamStudent findBySchoolIdAndSubjectCodeAndStudentCodeAndRemark(Integer schoolId, String subjectCode,
+            String studentCode, String examSeqCode);
+
+    ExamStudent findByExamIdAndSchoolIdAndSubjectCodeAndStudentCode(Integer examId, Integer schoolId,
+            String subjectCode, String studentCode);
+
+    ExamStudent findByExamIdAndSubjectCodeAndStudentCode(Integer examId, String subjectCode, String studentCode);
+
+    public List<Object[]> statisticsByExamIdAndSubjectCode(Integer examId, String code, Boolean upload, boolean absent);
+
+    public List<Object[]> statisticsByExamIdAndSubjectCode(Integer examId, String code);
+
+    public Long countByExamIdAndSubjectCodeAndCampus(Integer examId, String code, String campusName, boolean upload,
+            boolean absent);
+
+    public ExamStudent findBySchoolIdAndSubjectCodeStartingWithAndStudentCode(Integer schoolId, String subjectCode,
+            String studentCode);
+
+    public String findIdsByMarkLogin(String markLogin);
+
+    public String findIdsByMarkName(String markName);
+
+    List<ExamStudent> findByExamId(int examId, int pageNumber, int pageSize);
+
+    List<Object[]> statisticsByAbsentAndBreach(Integer examId, String code, Boolean upload, boolean absent,
+            boolean breach);
+
+    List<PictureTag> buildSheetTags(ExamStudent student, int index);
+
+    Map<Integer, List<PictureTag>> buildSheetTags(ExamStudent student);
+
+    List<OriginTag> getSliceTags(ExamStudent student);
+
+}

+ 877 - 679
stmms-biz/src/main/java/cn/com/qmth/stmms/biz/exam/service/impl/ExamStudentServiceImpl.java

@@ -1,679 +1,877 @@
-package cn.com.qmth.stmms.biz.exam.service.impl;
-
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-import javax.persistence.criteria.CriteriaBuilder;
-import javax.persistence.criteria.CriteriaQuery;
-import javax.persistence.criteria.Expression;
-import javax.persistence.criteria.Predicate;
-import javax.persistence.criteria.Root;
-
-import org.apache.commons.lang.StringUtils;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.data.domain.Page;
-import org.springframework.data.jpa.domain.Specification;
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-
-import cn.com.qmth.stmms.biz.campus.model.Campus;
-import cn.com.qmth.stmms.biz.campus.service.CampusService;
-import cn.com.qmth.stmms.biz.common.BaseQueryService;
-import cn.com.qmth.stmms.biz.exam.dao.ExamStudentDao;
-import cn.com.qmth.stmms.biz.exam.model.Exam;
-import cn.com.qmth.stmms.biz.exam.model.ExamPackage;
-import cn.com.qmth.stmms.biz.exam.model.ExamStudent;
-import cn.com.qmth.stmms.biz.exam.model.ExamSubject;
-import cn.com.qmth.stmms.biz.exam.query.ExamStudentSearchQuery;
-import cn.com.qmth.stmms.biz.exam.service.ExamPackageService;
-import cn.com.qmth.stmms.biz.exam.service.ExamService;
-import cn.com.qmth.stmms.biz.exam.service.ExamStudentService;
-import cn.com.qmth.stmms.biz.exam.service.ExamSubjectService;
-import cn.com.qmth.stmms.biz.user.model.User;
-import cn.com.qmth.stmms.biz.user.service.UserService;
-import cn.com.qmth.stmms.common.enums.ExamSubjectStatus;
-import cn.com.qmth.stmms.common.enums.Role;
-import cn.com.qmth.stmms.common.enums.UserType;
-import cn.com.qmth.stmms.common.utils.Md5EncryptUtils;
-
-@Service
-public class ExamStudentServiceImpl extends BaseQueryService<ExamStudent> implements ExamStudentService {
-
-    @Autowired
-    private ExamStudentDao studentDao;
-
-    @Autowired
-    private CampusService campusService;
-
-    @Autowired
-    private ExamPackageService packageService;
-
-    @Autowired
-    private ExamSubjectService subjectService;
-
-    @Autowired
-    private ExamService examService;
-
-    @Autowired
-    private UserService userService;
-
-    public static final String LOGINNAME_SPLITE = "-";
-
-    public static final String USER_PASSWORD = "123456";
-
-    @Value("${subject.name.bracket.replace}")
-    private boolean replaceBracket;
-
-    public ExamStudent findById(int id) {
-        return studentDao.findOne(id);
-    }
-
-    /**
-     * 批量添加考生
-     * 
-     * @param list
-     * @return
-     */
-    @Transactional
-    public int batchSave(List<ExamStudent> list) {
-        if (list == null || list.isEmpty()) {
-            return 0;
-        }
-        int success = 0;
-        int examId = list.get(0).getExamId();
-        int schoolId = list.get(0).getSchoolId();
-        Map<String, ExamSubject> subjectMap = new HashMap<String, ExamSubject>();
-        Set<String> campusSet = new HashSet<String>();
-        Set<String> packageSet = new HashSet<String>();
-
-        for (ExamStudent student : list) {
-            if (replaceBracket) {
-                String subjectName = student.getSubjectName().replaceAll("\\(", "(").replaceAll("\\)", ")");
-                student.setSubjectName(subjectName);
-            }
-            if (!subjectMap.containsKey(student.getSubjectCode())) {
-                ExamSubject subject = new ExamSubject();
-                subject.setCode(student.getSubjectCode());
-                subject.setName(student.getSubjectName());
-                subject.setRemark(StringUtils.trimToNull(student.getSubjectRemark()));
-                subject.setLevel(StringUtils.trimToNull(student.getSubjectLevel()));
-                subject.setCategory(StringUtils.trimToNull(student.getSubjectCategory()));
-                subjectMap.put(subject.getCode(), subject);
-            }
-            campusSet.add(student.getCampusName());
-            if (StringUtils.isNotBlank(student.getPackageCode())) {
-                packageSet.add(student.getPackageCode());
-            }
-            success++;
-        }
-
-        for (ExamSubject es : subjectMap.values()) {
-            ExamSubject subject = subjectService.find(examId, es.getCode());
-            if (subject == null) {
-                subject = new ExamSubject();
-                subject.setExamId(examId);
-                subject.setCode(es.getCode());
-                subject.setName(es.getName());
-                subject.setLevel(es.getLevel());
-                subject.setCategory(es.getCategory());
-                subject.setStatus(ExamSubjectStatus.MARKING);
-                subject.setObjectiveScore(0d);
-                subject.setSubjectiveScore(0d);
-                subject.setTotalScore(0d);
-                subject.setLibraryCount(0);
-                subject.setMarkedCount(0);
-                subject.setLeftCount(0);
-                subject.setHasAnswer(false);
-                subject.setHasPaper(false);
-                subject.setRemark(es.getRemark());
-
-                subjectService.save(subject);
-                // 增加科组长
-                User user = new User();
-                StringBuilder name = new StringBuilder();
-                name.append(examId).append(LOGINNAME_SPLITE).append(es.getCode());
-                user.setSchoolId(schoolId);
-                user.setLoginName(name.toString());
-                user.setName(name.toString());
-                user.setStatus(1);
-                user.setType(UserType.VIEWER);
-                user.setRoleNames(Role.SUBJECT_VIEWER.getName());
-                user.setPassword(Md5EncryptUtils.md5(USER_PASSWORD));
-                user.setCreatedTime(new Date());
-                userService.save(user);
-            }
-        }
-
-        for (String name : campusSet) {
-            Campus campus = campusService.findBySchoolAndName(schoolId, name);
-            if (campus == null) {
-                campus = new Campus();
-                campus.setSchoolId(schoolId);
-                campus.setName(name);
-                campusService.save(campus);
-            }
-        }
-
-        for (String code : packageSet) {
-            ExamPackage examPackage = packageService.find(examId, code);
-            if (examPackage == null) {
-                examPackage = new ExamPackage();
-                examPackage.setExamId(examId);
-                examPackage.setCode(code);
-                examPackage.setPicCount(0);
-                packageService.save(examPackage);
-            }
-        }
-
-        studentDao.save(list);
-        return success;
-    }
-
-    @Transactional
-    public ExamStudent save(ExamStudent student) {
-        ExamSubject subject = subjectService.find(student.getExamId(), student.getSubjectCode());
-        if (subject == null) {
-            subject = new ExamSubject();
-            subject.setExamId(student.getExamId());
-            subject.setCode(student.getSubjectCode());
-            subject.setName(student.getSubjectName());
-            subject.setLevel(StringUtils.trimToNull(student.getSubjectLevel()));
-            subject.setCategory(StringUtils.trimToNull(student.getSubjectCategory()));
-            subject.setStatus(ExamSubjectStatus.MARKING);
-            subject.setObjectiveScore(0d);
-            subject.setSubjectiveScore(0d);
-            subject.setTotalScore(0d);
-            subject.setLibraryCount(0);
-            subject.setMarkedCount(0);
-            subject.setLeftCount(0);
-            subject.setHasAnswer(false);
-            subject.setHasPaper(false);
-            subject.setRemark(StringUtils.trimToNull(student.getSubjectRemark()));
-            subjectService.save(subject);
-        }
-
-        Campus campus = campusService.findBySchoolAndName(student.getSchoolId(), student.getCampusName());
-        if (campus == null) {
-            campus = new Campus();
-            campus.setSchoolId(student.getSchoolId());
-            campus.setName(student.getCampusName());
-            campusService.save(campus);
-        }
-
-        if (StringUtils.isNotBlank(student.getPackageCode())) {
-            ExamPackage examPackage = packageService.find(student.getExamId(), student.getPackageCode());
-            if (examPackage == null) {
-                examPackage = new ExamPackage();
-                examPackage.setExamId(student.getExamId());
-                examPackage.setCode(student.getPackageCode());
-                examPackage.setPicCount(0);
-                packageService.save(examPackage);
-            }
-        }
-        if (replaceBracket) {
-            student.setSubjectName(subject.getName());
-        }
-        return studentDao.save(student);
-    }
-
-    @Transactional
-    public void deleteById(int id) {
-        ExamStudent student = findById(id);
-        if (student != null) {
-            delete(student);
-        }
-    }
-
-    @Transactional
-    public void delete(ExamStudent student) {
-        studentDao.delete(student);
-    }
-
-    @Transactional
-    public void deleteByExamId(int examId) {
-        studentDao.deleteByExamId(examId);
-    }
-
-    public ExamStudentSearchQuery findByQuery(final ExamStudentSearchQuery query) {
-        checkQuery(query);
-        Page<ExamStudent> result = studentDao.findAll(buildSpecification(query), query);
-        fillResult(result, query);
-        return query;
-    }
-
-    public long countByQuery(final ExamStudentSearchQuery query) {
-        return studentDao.count(buildSpecification(query));
-    }
-
-    @Override
-    public List<ExamStudent> findByExamId(int examId) {
-        return studentDao.findByExamId(examId, null);
-    }
-
-    @Override
-    public List<ExamStudent> findByExamId(int examId, int pageNumber, int pageSize) {
-        ExamStudentSearchQuery query = new ExamStudentSearchQuery();
-        query.setPageNumber(pageNumber);
-        query.setPageSize(pageSize);
-        return studentDao.findByExamId(examId, query);
-    }
-
-    @Override
-    public List<ExamStudent> findByExamIdAndUploadAndAbsent(int examId, boolean upload, boolean absent, int pageNumber,
-            int pageSize) {
-        ExamStudentSearchQuery query = new ExamStudentSearchQuery();
-        query.setPageNumber(pageNumber);
-        query.setPageSize(pageSize);
-        return studentDao.findByExamIdAndUploadAndAbsent(examId, upload, absent, query);
-    }
-
-    @Override
-    public List<ExamStudent> findByExamIdAndUpload(int examId, boolean upload, int pageNumber, int pageSize) {
-        ExamStudentSearchQuery query = new ExamStudentSearchQuery();
-        query.setPageNumber(pageNumber);
-        query.setPageSize(pageSize);
-        return studentDao.findByExamIdAndUpload(examId, upload, query);
-    }
-
-    @Override
-    public List<ExamStudent> findByExamIdAndCampusName(int examId, String campusName, int pageNumber, int pageSize) {
-        ExamStudentSearchQuery query = new ExamStudentSearchQuery();
-        query.setPageNumber(pageNumber);
-        query.setPageSize(pageSize);
-        return studentDao.findByExamIdAndCampusName(examId, campusName, query);
-    }
-
-    @Override
-    public ExamStudent findByExamIdAndExamNumber(int examId, String examNumber) {
-        List<ExamStudent> list = studentDao.findByExamIdAndExamNumber(examId, examNumber);
-        return list != null && list.size() > 0 ? list.get(0) : null;
-    }
-
-    public long countByExamId(int examId) {
-        ExamStudentSearchQuery query = new ExamStudentSearchQuery();
-        query.setExamId(examId);
-        return countByQuery(query);
-    }
-
-    @Override
-    public long countByExamIdAndUpload(int examId, boolean upload) {
-        ExamStudentSearchQuery query = new ExamStudentSearchQuery();
-        query.setExamId(examId);
-        query.setUpload(upload);
-        return countByQuery(query);
-    }
-
-    public long countByExamIdAndSubjectCode(int examId, String subjectCode) {
-        ExamStudentSearchQuery query = new ExamStudentSearchQuery();
-        query.setExamId(examId);
-        query.setSubjectCode(subjectCode);
-        return countByQuery(query);
-    }
-
-    public long countByExamIdAndSubjectCode(int examId, String subjectCode, boolean upload) {
-        ExamStudentSearchQuery query = new ExamStudentSearchQuery();
-        query.setExamId(examId);
-        query.setSubjectCode(subjectCode);
-        query.setUpload(upload);
-        return countByQuery(query);
-    }
-
-    public long countByExamIdAndSubjectCode(int examId, String subjectCode, boolean upload, boolean absent) {
-        ExamStudentSearchQuery query = new ExamStudentSearchQuery();
-        query.setExamId(examId);
-        query.setSubjectCode(subjectCode);
-        query.setUpload(upload);
-        query.setAbsent(absent);
-        return countByQuery(query);
-    }
-
-    public long countByNoAbsentAndBreach(int examId, String subjectCode, boolean upload, boolean absent,
-            boolean breach) {
-        ExamStudentSearchQuery query = new ExamStudentSearchQuery();
-        query.setExamId(examId);
-        query.setSubjectCode(subjectCode);
-        query.setUpload(upload);
-        query.setAbsent(absent);
-        query.setBreach(breach);
-        return countByQuery(query);
-    }
-
-    public long countByAbsentAndBreach(int examId, String subjectCode, Boolean absent, Boolean breach) {
-        ExamStudentSearchQuery query = new ExamStudentSearchQuery();
-        query.setExamId(examId);
-        query.setSubjectCode(subjectCode);
-        if (breach != null) {
-            query.setBreach(breach);
-        }
-        if (absent != null) {
-            query.setAbsent(absent);
-        }
-        return countByQuery(query);
-    }
-
-    @Override
-    public long countByExamIdAndCampusName(int examId, String campusName) {
-        ExamStudentSearchQuery query = new ExamStudentSearchQuery();
-        query.setExamId(examId);
-        query.setCampusName(campusName);
-        return countByQuery(query);
-    }
-
-    @Override
-    public long countByExamIdAndCampusName(int examId, String campusName, boolean upload) {
-        ExamStudentSearchQuery query = new ExamStudentSearchQuery();
-        query.setExamId(examId);
-        query.setCampusName(campusName);
-        query.setUpload(upload);
-        return countByQuery(query);
-    }
-
-    @Override
-    public long countCampusByExam(int examId) {
-        return studentDao.countCampusNameByExamId(examId);
-    }
-
-    @Override
-    public List<String> findDistinctCampusName(int examId) {
-        return studentDao.findDistinctCampusName(examId);
-    }
-
-    @Override
-    public ExamStudentSearchQuery findDistinctCampusName(ExamStudentSearchQuery query) {
-        checkQuery(query);
-        Page<ExamStudent> result = studentDao.findDistinctCampusName(query.getExamId(), query);
-        fillResult(result, query);
-        return query;
-    }
-
-    @Override
-    public List<String> findDistinctPackageCode(int examId) {
-        return studentDao.findDistinctPackageCode(examId);
-    }
-
-    @Override
-    @Transactional
-    public void updateSubjectiveScore(int id, double score, String scoreList) {
-        studentDao.updateSubjectiveScore(id, score, scoreList);
-    }
-
-    @Override
-    @Transactional
-    public void updateManualAbsent(int id, boolean manualAbsent) {
-        studentDao.updateManualAbsent(id, manualAbsent);
-    }
-
-    @Override
-    @Transactional
-    public void updateManualAbsent(int examId, String examNumber, boolean manualAbsent) {
-        studentDao.updateManualAbsent(examId, examNumber, manualAbsent);
-    }
-
-    @Override
-    @Transactional
-    public void clearManualAbsent(int examId) {
-        studentDao.clearManualAbsent(examId);
-    }
-
-    @Override
-    @Transactional
-    public void updateException(int id, boolean exception) {
-        studentDao.updateException(id, exception);
-    }
-
-    private Specification<ExamStudent> buildSpecification(final ExamStudentSearchQuery query) {
-        return new Specification<ExamStudent>() {
-
-            @Override
-            public Predicate toPredicate(Root<ExamStudent> root, CriteriaQuery<?> cQuery, CriteriaBuilder cb) {
-                List<Predicate> predicates = new LinkedList<Predicate>();
-                Expression<Double> evaluationItemSum = cb.sum(root.get("objectiveScore").as(Double.class),
-                        root.get("subjectiveScore").as(Double.class));
-                if (query.getStartScroe() != null) {
-                    Predicate predicate1 = cb.ge(evaluationItemSum, query.getStartScroe());
-                    Predicate predicate2 = cb.le(evaluationItemSum, query.getEndScroe());
-                    if (query.getStartScroe() == 0) {
-                        Predicate predicate = cb.or(cb.equal(root.get("absent"), true),
-                                cb.equal(root.get("breach"), true));
-                        Predicate predicate3 = cb.and(predicate1, predicate2);
-                        predicates.add(cb.or(predicate, predicate3));
-                    } else {
-                        predicates.add(cb.equal(root.get("absent"), false));
-                        predicates.add(cb.equal(root.get("breach"), false));
-                        predicates.add(cb.and(predicate1, predicate2));
-                    }
-                }
-                // if (query.getStartScroe()!=null && query.getEndScroe()!=null
-                // ) {
-                // predicates.add(cb.between(evaluationItemSum,
-                // query.getStartScroe(), query.getEndScroe()));
-                // }
-                if (query.getExamId() > 0) {
-                    predicates.add(cb.equal(root.get("examId"), query.getExamId()));
-                }
-                if (StringUtils.isNotBlank(query.getIds())) {
-                    String[] ids = query.getIds().split(",");
-                    List<String> list = new ArrayList<String>();
-                    for (String str : ids) {
-                        list.add(str);
-                    }
-                    predicates.add(root.get("id").in(list));
-                }
-                if (StringUtils.isNotBlank(query.getExamNumber())) {
-                    predicates.add(cb.equal(root.get("examNumber"), query.getExamNumber()));
-                } else if (StringUtils.isNotBlank(query.getExamNumberIn())) {
-                    String[] list = query.getExamNumberIn().split(",");
-                    if (list.length > 0) {
-                        Predicate[] sub = new Predicate[list.length];
-                        for (int i = 0; i < list.length; i++) {
-                            sub[i] = cb.equal(root.get("examNumber"), list[i]);
-                        }
-                        predicates.add(cb.or(sub));
-                    }
-                }
-                if (StringUtils.isNotBlank(query.getStudentCode())) {
-                    predicates.add(cb.equal(root.get("studentCode"), query.getStudentCode()));
-                }
-                if (StringUtils.isNotBlank(query.getSubjectCode())) {
-                    predicates.add(cb.equal(root.get("subjectCode"), query.getSubjectCode()));
-                }
-                if (StringUtils.isNotBlank(query.getCampusName())) {
-                    predicates.add(cb.equal(root.get("campusName"), query.getCampusName()));
-                }
-                if (StringUtils.isNotBlank(query.getName())) {
-                    predicates.add(cb.like(root.get("name").as(String.class), query.getName() + "%"));
-                }
-                if (StringUtils.isNotBlank(query.getBatchCode())) {
-                    predicates.add(cb.equal(root.get("batchCode"), query.getBatchCode()));
-                }
-                if (StringUtils.isNotBlank(query.getPackageCode())) {
-                    predicates.add(cb.equal(root.get("packageCode"), query.getPackageCode()));
-                }
-                if (StringUtils.isNotBlank(query.getSubjectLevel())) {
-                    predicates.add(cb.equal(root.get("subjectLevel"), query.getSubjectLevel()));
-                }
-                if (StringUtils.isNotBlank(query.getSubjectCategory())) {
-                    predicates.add(cb.equal(root.get("subjectCategory"), query.getSubjectCategory()));
-                }
-                if (query.getObjectiveScore() != null) {
-                    predicates.add(cb.equal(root.get("objectiveScore"), query.getObjectiveScore()));
-                } else if (query.getObjectiveScoreGt() != null) {
-                    predicates.add(
-                            cb.greaterThan(root.get("objectiveScore").as(Double.class), query.getObjectiveScoreGt()));
-                } else if (query.getObjectiveScoreLt() != null) {
-                    predicates
-                            .add(cb.lessThan(root.get("objectiveScore").as(Double.class), query.getObjectiveScoreLt()));
-                }
-                if (query.getSubjectiveScore() != null) {
-                    predicates.add(cb.equal(root.get("subjectiveScore"), query.getSubjectiveScore()));
-                } else if (query.getSubjectiveScoreGt() != null) {
-                    predicates.add(
-                            cb.greaterThan(root.get("subjectiveScore").as(Double.class), query.getSubjectiveScoreGt()));
-                } else if (query.getSubjectiveScoreLt() != null) {
-                    predicates.add(
-                            cb.lessThan(root.get("subjectiveScore").as(Double.class), query.getSubjectiveScoreLt()));
-                }
-                if (query.getUpload() != null) {
-                    predicates.add(cb.equal(root.get("upload"), query.getUpload()));
-                }
-                if (query.getAbsent() != null) {
-                    if (query.getAbsent()) {// 缺考=缺考+ 未上传
-                        predicates.add(cb.or(cb.equal(root.get("absent"), true), cb.equal(root.get("upload"), false)));
-                    } else {
-                        predicates.add(cb.equal(root.get("absent"), query.getAbsent()));
-                        predicates.add(cb.equal(root.get("upload"), true));
-                    }
-                }
-                if (query.getManualAbsent() != null) {
-                    predicates.add(cb.equal(root.get("manualAbsent"), query.getManualAbsent()));
-                }
-                if (query.getBreach() != null) {
-                    predicates.add(cb.equal(root.get("breach"), query.getBreach()));
-                }
-                if (query.getException() != null) {
-                    predicates.add(cb.equal(root.get("exception"), query.getException()));
-                }
-                if (StringUtils.isNotBlank(query.getSubjectCodeIn())) {
-                    String[] list = query.getSubjectCodeIn().split(",");
-                    if (list.length > 0) {
-                        Predicate[] sub = new Predicate[list.length];
-                        for (int i = 0; i < list.length; i++) {
-                            sub[i] = cb.equal(root.get("subjectCode"), list[i]);
-                        }
-                        predicates.add(cb.or(sub));
-                    }
-                }
-                if (StringUtils.isNotBlank(query.getCampusNameIn())) {
-                    String[] list = query.getCampusNameIn().split(",");
-                    if (list.length > 0) {
-                        Predicate[] sub = new Predicate[list.length];
-                        for (int i = 0; i < list.length; i++) {
-                            sub[i] = cb.equal(root.get("campusName"), list[i]);
-                        }
-                        predicates.add(cb.or(sub));
-                    }
-                }
-                if (StringUtils.isNotBlank(query.getSubjectCodeNotIn())) {
-                    String[] list = query.getSubjectCodeNotIn().split(",");
-                    if (list.length > 0) {
-                        Predicate[] sub = new Predicate[list.length];
-                        for (int i = 0; i < list.length; i++) {
-                            sub[i] = cb.notEqual(root.get("subjectCode"), list[i]);
-                        }
-                        predicates.add(cb.and(sub));
-                    }
-                }
-                if (StringUtils.isNotBlank(query.getCampusNameNotIn())) {
-                    String[] list = query.getCampusNameNotIn().split(",");
-                    if (list.length > 0) {
-                        Predicate[] sub = new Predicate[list.length];
-                        for (int i = 0; i < list.length; i++) {
-                            sub[i] = cb.notEqual(root.get("campusName"), list[i]);
-                        }
-                        predicates.add(cb.and(sub));
-                    }
-                }
-                if (query.getStudentId() != null) {
-                    predicates.add(cb.equal(root.get("id"), query.getStudentId()));
-                }
-                return predicates.isEmpty() ? cb.conjunction()
-                        : cb.and(predicates.toArray(new Predicate[predicates.size()]));
-            }
-        };
-
-    }
-
-    @Override
-    public ExamStudent findBySchoolIdAndSubjectCodeAndStudentCode(Integer schoolId, String subjectCode,
-            String studentCode) {
-        List<Exam> exams = examService.findBySchoolId(schoolId);
-        if (exams != null && exams.size() > 0) {
-            return studentDao.findByExamIdAndSubjectCodeAndStudentCode(exams.get(0).getId(), subjectCode, studentCode);
-        } else {
-            return null;
-        }
-    }
-
-    @Override
-    public ExamStudent findBySchoolIdAndSubjectCodeAndStudentCodeAndRemark(Integer schoolId, String subjectCode,
-            String studentCode, String examSeqCode) {
-        return studentDao.findBySchoolIdAndSubjectCodeAndStudentCodeAndRemark(schoolId, subjectCode, studentCode,
-                examSeqCode);
-    }
-
-    @Override
-    public ExamStudent findByExamIdAndSubjectCodeAndStudentCode(Integer examId, String subjectCode,
-            String studentCode) {
-        return studentDao.findByExamIdAndSubjectCodeAndStudentCode(examId, subjectCode, studentCode);
-    }
-
-    @Override
-    public ExamStudent findByExamIdAndSchoolIdAndSubjectCodeAndStudentCode(Integer examId, Integer schoolId,
-            String subjectCode, String studentCode) {
-        return studentDao.findByExamIdAndSchoolIdAndSubjectCodeAndStudentCode(examId, schoolId, subjectCode,
-                studentCode);
-    }
-
-    @Override
-    public List<Object[]> statisticsByExamIdAndSubjectCode(Integer examId, String code, Boolean upload,
-            boolean absent) {
-        return studentDao.statisticsByExamIdAndSubjectCode(examId, code, upload, absent);
-    }
-
-    @Override
-    public List<Object[]> statisticsByExamIdAndSubjectCode(Integer examId, String code) {
-        return studentDao.statisticsByExamIdAndSubjectCode(examId, code);
-    }
-
-    @Override
-    public Long countByExamIdAndSubjectCodeAndCampus(Integer examId, String subjectCode, String campusName,
-            boolean upload, boolean absent) {
-        ExamStudentSearchQuery query = new ExamStudentSearchQuery();
-        query.setExamId(examId);
-        query.setSubjectCode(subjectCode);
-        query.setCampusName(campusName);
-        query.setUpload(upload);
-        query.setAbsent(absent);
-        return countByQuery(query);
-    }
-
-    @Override
-    public ExamStudent findBySchoolIdAndSubjectCodeStartingWithAndStudentCode(Integer schoolId, String subjectCode,
-            String studentCode) {
-        List<Exam> exams = examService.findBySchoolId(schoolId);
-        if (exams != null && exams.size() > 0) {
-            return studentDao.findByExamIdAndSubjectCodeStartingWithAndStudentCode(exams.get(0).getId(), subjectCode,
-                    studentCode);
-        } else {
-            return null;
-        }
-    }
-
-    @Override
-    public String findIdsByMarkLogin(String markLogin) {
-        List<String[]> o = studentDao.findIdsByMarkLogin(markLogin);
-        return StringUtils.join(o, ",");
-    }
-
-    @Override
-    public String findIdsByMarkName(String MarkName) {
-        List<String[]> o = studentDao.findIdsByMarkName(MarkName);
-        return StringUtils.join(o, ",");
-    }
-
-    @Override
-    /**
-     * 根据缺考和违纪字段联合查询
-     */
-    public List<Object[]> statisticsByAbsentAndBreach(Integer examId, String code, Boolean upload, boolean absent,
-            boolean breach) {
-        return studentDao.statisticsByAbsentAndBreach(examId, code, upload, absent, breach);
-    }
-}
+package cn.com.qmth.stmms.biz.exam.service.impl;
+
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.persistence.criteria.CriteriaBuilder;
+import javax.persistence.criteria.CriteriaQuery;
+import javax.persistence.criteria.Expression;
+import javax.persistence.criteria.Predicate;
+import javax.persistence.criteria.Root;
+
+import org.apache.commons.lang.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.domain.Page;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import cn.com.qmth.stmms.biz.campus.model.Campus;
+import cn.com.qmth.stmms.biz.campus.service.CampusService;
+import cn.com.qmth.stmms.biz.common.BaseQueryService;
+import cn.com.qmth.stmms.biz.exam.dao.ExamStudentDao;
+import cn.com.qmth.stmms.biz.exam.model.Exam;
+import cn.com.qmth.stmms.biz.exam.model.ExamPackage;
+import cn.com.qmth.stmms.biz.exam.model.ExamQuestion;
+import cn.com.qmth.stmms.biz.exam.model.ExamStudent;
+import cn.com.qmth.stmms.biz.exam.model.ExamSubject;
+import cn.com.qmth.stmms.biz.exam.query.ExamStudentSearchQuery;
+import cn.com.qmth.stmms.biz.exam.service.ExamPackageService;
+import cn.com.qmth.stmms.biz.exam.service.ExamQuestionService;
+import cn.com.qmth.stmms.biz.exam.service.ExamService;
+import cn.com.qmth.stmms.biz.exam.service.ExamStudentService;
+import cn.com.qmth.stmms.biz.exam.service.ExamSubjectService;
+import cn.com.qmth.stmms.biz.mark.model.MarkLibrary;
+import cn.com.qmth.stmms.biz.mark.model.MarkSpecialTag;
+import cn.com.qmth.stmms.biz.mark.model.MarkTrack;
+import cn.com.qmth.stmms.biz.mark.model.PictureConfigItem;
+import cn.com.qmth.stmms.biz.mark.service.MarkLibraryService;
+import cn.com.qmth.stmms.biz.mark.service.MarkSpecialTagService;
+import cn.com.qmth.stmms.biz.mark.service.MarkTrackService;
+import cn.com.qmth.stmms.biz.user.model.User;
+import cn.com.qmth.stmms.biz.user.service.UserService;
+import cn.com.qmth.stmms.biz.utils.OriginTag;
+import cn.com.qmth.stmms.biz.utils.PictureConfigTransform;
+import cn.com.qmth.stmms.biz.utils.PictureTag;
+import cn.com.qmth.stmms.biz.utils.ScoreItem;
+import cn.com.qmth.stmms.common.enums.ExamSubjectStatus;
+import cn.com.qmth.stmms.common.enums.Role;
+import cn.com.qmth.stmms.common.enums.UserType;
+import cn.com.qmth.stmms.common.utils.Md5EncryptUtils;
+
+@Service
+public class ExamStudentServiceImpl extends BaseQueryService<ExamStudent> implements ExamStudentService {
+
+    @Autowired
+    private ExamStudentDao studentDao;
+
+    @Autowired
+    private CampusService campusService;
+
+    @Autowired
+    private ExamPackageService packageService;
+
+    @Autowired
+    private ExamSubjectService subjectService;
+
+    @Autowired
+    private ExamService examService;
+
+    @Autowired
+    private UserService userService;
+
+    @Autowired
+    private MarkLibraryService libraryService;
+
+    @Autowired
+    private ExamQuestionService questionService;
+
+    @Autowired
+    private MarkTrackService trackService;
+
+    @Autowired
+    private MarkSpecialTagService specialTagService;
+
+    public static final String LOGINNAME_SPLITE = "-";
+
+    public static final String USER_PASSWORD = "123456";
+
+    @Value("${subject.name.bracket.replace}")
+    private boolean replaceBracket;
+
+    public ExamStudent findById(int id) {
+        return studentDao.findOne(id);
+    }
+
+    /**
+     * 批量添加考生
+     * 
+     * @param list
+     * @return
+     */
+    @Transactional
+    public int batchSave(List<ExamStudent> list) {
+        if (list == null || list.isEmpty()) {
+            return 0;
+        }
+        int success = 0;
+        int examId = list.get(0).getExamId();
+        int schoolId = list.get(0).getSchoolId();
+        Map<String, ExamSubject> subjectMap = new HashMap<String, ExamSubject>();
+        Set<String> campusSet = new HashSet<String>();
+        Set<String> packageSet = new HashSet<String>();
+
+        for (ExamStudent student : list) {
+            if (replaceBracket) {
+                String subjectName = student.getSubjectName().replaceAll("\\(", "(").replaceAll("\\)", ")");
+                student.setSubjectName(subjectName);
+            }
+            if (!subjectMap.containsKey(student.getSubjectCode())) {
+                ExamSubject subject = new ExamSubject();
+                subject.setCode(student.getSubjectCode());
+                subject.setName(student.getSubjectName());
+                subject.setRemark(StringUtils.trimToNull(student.getSubjectRemark()));
+                subject.setLevel(StringUtils.trimToNull(student.getSubjectLevel()));
+                subject.setCategory(StringUtils.trimToNull(student.getSubjectCategory()));
+                subjectMap.put(subject.getCode(), subject);
+            }
+            campusSet.add(student.getCampusName());
+            if (StringUtils.isNotBlank(student.getPackageCode())) {
+                packageSet.add(student.getPackageCode());
+            }
+            success++;
+        }
+
+        for (ExamSubject es : subjectMap.values()) {
+            ExamSubject subject = subjectService.find(examId, es.getCode());
+            if (subject == null) {
+                subject = new ExamSubject();
+                subject.setExamId(examId);
+                subject.setCode(es.getCode());
+                subject.setName(es.getName());
+                subject.setLevel(es.getLevel());
+                subject.setCategory(es.getCategory());
+                subject.setStatus(ExamSubjectStatus.MARKING);
+                subject.setObjectiveScore(0d);
+                subject.setSubjectiveScore(0d);
+                subject.setTotalScore(0d);
+                subject.setLibraryCount(0);
+                subject.setMarkedCount(0);
+                subject.setLeftCount(0);
+                subject.setHasAnswer(false);
+                subject.setHasPaper(false);
+                subject.setRemark(es.getRemark());
+
+                subjectService.save(subject);
+                // 增加科组长
+                User user = new User();
+                StringBuilder name = new StringBuilder();
+                name.append(examId).append(LOGINNAME_SPLITE).append(es.getCode());
+                user.setSchoolId(schoolId);
+                user.setLoginName(name.toString());
+                user.setName(name.toString());
+                user.setStatus(1);
+                user.setType(UserType.VIEWER);
+                user.setRoleNames(Role.SUBJECT_VIEWER.getName());
+                user.setPassword(Md5EncryptUtils.md5(USER_PASSWORD));
+                user.setCreatedTime(new Date());
+                userService.save(user);
+            }
+        }
+
+        for (String name : campusSet) {
+            Campus campus = campusService.findBySchoolAndName(schoolId, name);
+            if (campus == null) {
+                campus = new Campus();
+                campus.setSchoolId(schoolId);
+                campus.setName(name);
+                campusService.save(campus);
+            }
+        }
+
+        for (String code : packageSet) {
+            ExamPackage examPackage = packageService.find(examId, code);
+            if (examPackage == null) {
+                examPackage = new ExamPackage();
+                examPackage.setExamId(examId);
+                examPackage.setCode(code);
+                examPackage.setPicCount(0);
+                packageService.save(examPackage);
+            }
+        }
+
+        studentDao.save(list);
+        return success;
+    }
+
+    @Transactional
+    public ExamStudent save(ExamStudent student) {
+        ExamSubject subject = subjectService.find(student.getExamId(), student.getSubjectCode());
+        if (subject == null) {
+            subject = new ExamSubject();
+            subject.setExamId(student.getExamId());
+            subject.setCode(student.getSubjectCode());
+            subject.setName(student.getSubjectName());
+            subject.setLevel(StringUtils.trimToNull(student.getSubjectLevel()));
+            subject.setCategory(StringUtils.trimToNull(student.getSubjectCategory()));
+            subject.setStatus(ExamSubjectStatus.MARKING);
+            subject.setObjectiveScore(0d);
+            subject.setSubjectiveScore(0d);
+            subject.setTotalScore(0d);
+            subject.setLibraryCount(0);
+            subject.setMarkedCount(0);
+            subject.setLeftCount(0);
+            subject.setHasAnswer(false);
+            subject.setHasPaper(false);
+            subject.setRemark(StringUtils.trimToNull(student.getSubjectRemark()));
+            subjectService.save(subject);
+        }
+
+        Campus campus = campusService.findBySchoolAndName(student.getSchoolId(), student.getCampusName());
+        if (campus == null) {
+            campus = new Campus();
+            campus.setSchoolId(student.getSchoolId());
+            campus.setName(student.getCampusName());
+            campusService.save(campus);
+        }
+
+        if (StringUtils.isNotBlank(student.getPackageCode())) {
+            ExamPackage examPackage = packageService.find(student.getExamId(), student.getPackageCode());
+            if (examPackage == null) {
+                examPackage = new ExamPackage();
+                examPackage.setExamId(student.getExamId());
+                examPackage.setCode(student.getPackageCode());
+                examPackage.setPicCount(0);
+                packageService.save(examPackage);
+            }
+        }
+        if (replaceBracket) {
+            student.setSubjectName(subject.getName());
+        }
+        return studentDao.save(student);
+    }
+
+    @Transactional
+    public void deleteById(int id) {
+        ExamStudent student = findById(id);
+        if (student != null) {
+            delete(student);
+        }
+    }
+
+    @Transactional
+    public void delete(ExamStudent student) {
+        studentDao.delete(student);
+    }
+
+    @Transactional
+    public void deleteByExamId(int examId) {
+        studentDao.deleteByExamId(examId);
+    }
+
+    public ExamStudentSearchQuery findByQuery(final ExamStudentSearchQuery query) {
+        checkQuery(query);
+        Page<ExamStudent> result = studentDao.findAll(buildSpecification(query), query);
+        fillResult(result, query);
+        return query;
+    }
+
+    public long countByQuery(final ExamStudentSearchQuery query) {
+        return studentDao.count(buildSpecification(query));
+    }
+
+    @Override
+    public List<ExamStudent> findByExamId(int examId) {
+        return studentDao.findByExamId(examId, null);
+    }
+
+    @Override
+    public List<ExamStudent> findByExamId(int examId, int pageNumber, int pageSize) {
+        ExamStudentSearchQuery query = new ExamStudentSearchQuery();
+        query.setPageNumber(pageNumber);
+        query.setPageSize(pageSize);
+        return studentDao.findByExamId(examId, query);
+    }
+
+    @Override
+    public List<ExamStudent> findByExamIdAndUploadAndAbsent(int examId, boolean upload, boolean absent, int pageNumber,
+            int pageSize) {
+        ExamStudentSearchQuery query = new ExamStudentSearchQuery();
+        query.setPageNumber(pageNumber);
+        query.setPageSize(pageSize);
+        return studentDao.findByExamIdAndUploadAndAbsent(examId, upload, absent, query);
+    }
+
+    @Override
+    public List<ExamStudent> findByExamIdAndUpload(int examId, boolean upload, int pageNumber, int pageSize) {
+        ExamStudentSearchQuery query = new ExamStudentSearchQuery();
+        query.setPageNumber(pageNumber);
+        query.setPageSize(pageSize);
+        return studentDao.findByExamIdAndUpload(examId, upload, query);
+    }
+
+    @Override
+    public List<ExamStudent> findByExamIdAndCampusName(int examId, String campusName, int pageNumber, int pageSize) {
+        ExamStudentSearchQuery query = new ExamStudentSearchQuery();
+        query.setPageNumber(pageNumber);
+        query.setPageSize(pageSize);
+        return studentDao.findByExamIdAndCampusName(examId, campusName, query);
+    }
+
+    @Override
+    public ExamStudent findByExamIdAndExamNumber(int examId, String examNumber) {
+        List<ExamStudent> list = studentDao.findByExamIdAndExamNumber(examId, examNumber);
+        return list != null && list.size() > 0 ? list.get(0) : null;
+    }
+
+    public long countByExamId(int examId) {
+        ExamStudentSearchQuery query = new ExamStudentSearchQuery();
+        query.setExamId(examId);
+        return countByQuery(query);
+    }
+
+    @Override
+    public long countByExamIdAndUpload(int examId, boolean upload) {
+        ExamStudentSearchQuery query = new ExamStudentSearchQuery();
+        query.setExamId(examId);
+        query.setUpload(upload);
+        return countByQuery(query);
+    }
+
+    public long countByExamIdAndSubjectCode(int examId, String subjectCode) {
+        ExamStudentSearchQuery query = new ExamStudentSearchQuery();
+        query.setExamId(examId);
+        query.setSubjectCode(subjectCode);
+        return countByQuery(query);
+    }
+
+    public long countByExamIdAndSubjectCode(int examId, String subjectCode, boolean upload) {
+        ExamStudentSearchQuery query = new ExamStudentSearchQuery();
+        query.setExamId(examId);
+        query.setSubjectCode(subjectCode);
+        query.setUpload(upload);
+        return countByQuery(query);
+    }
+
+    public long countByExamIdAndSubjectCode(int examId, String subjectCode, boolean upload, boolean absent) {
+        ExamStudentSearchQuery query = new ExamStudentSearchQuery();
+        query.setExamId(examId);
+        query.setSubjectCode(subjectCode);
+        query.setUpload(upload);
+        query.setAbsent(absent);
+        return countByQuery(query);
+    }
+
+    public long countByNoAbsentAndBreach(int examId, String subjectCode, boolean upload, boolean absent,
+            boolean breach) {
+        ExamStudentSearchQuery query = new ExamStudentSearchQuery();
+        query.setExamId(examId);
+        query.setSubjectCode(subjectCode);
+        query.setUpload(upload);
+        query.setAbsent(absent);
+        query.setBreach(breach);
+        return countByQuery(query);
+    }
+
+    public long countByAbsentAndBreach(int examId, String subjectCode, Boolean absent, Boolean breach) {
+        ExamStudentSearchQuery query = new ExamStudentSearchQuery();
+        query.setExamId(examId);
+        query.setSubjectCode(subjectCode);
+        if (breach != null) {
+            query.setBreach(breach);
+        }
+        if (absent != null) {
+            query.setAbsent(absent);
+        }
+        return countByQuery(query);
+    }
+
+    @Override
+    public long countByExamIdAndCampusName(int examId, String campusName) {
+        ExamStudentSearchQuery query = new ExamStudentSearchQuery();
+        query.setExamId(examId);
+        query.setCampusName(campusName);
+        return countByQuery(query);
+    }
+
+    @Override
+    public long countByExamIdAndCampusName(int examId, String campusName, boolean upload) {
+        ExamStudentSearchQuery query = new ExamStudentSearchQuery();
+        query.setExamId(examId);
+        query.setCampusName(campusName);
+        query.setUpload(upload);
+        return countByQuery(query);
+    }
+
+    @Override
+    public long countCampusByExam(int examId) {
+        return studentDao.countCampusNameByExamId(examId);
+    }
+
+    @Override
+    public List<String> findDistinctCampusName(int examId) {
+        return studentDao.findDistinctCampusName(examId);
+    }
+
+    @Override
+    public ExamStudentSearchQuery findDistinctCampusName(ExamStudentSearchQuery query) {
+        checkQuery(query);
+        Page<ExamStudent> result = studentDao.findDistinctCampusName(query.getExamId(), query);
+        fillResult(result, query);
+        return query;
+    }
+
+    @Override
+    public List<String> findDistinctPackageCode(int examId) {
+        return studentDao.findDistinctPackageCode(examId);
+    }
+
+    @Override
+    @Transactional
+    public void updateSubjectiveScore(int id, double score, String scoreList) {
+        studentDao.updateSubjectiveScore(id, score, scoreList);
+    }
+
+    @Override
+    @Transactional
+    public void updateManualAbsent(int id, boolean manualAbsent) {
+        studentDao.updateManualAbsent(id, manualAbsent);
+    }
+
+    @Override
+    @Transactional
+    public void updateManualAbsent(int examId, String examNumber, boolean manualAbsent) {
+        studentDao.updateManualAbsent(examId, examNumber, manualAbsent);
+    }
+
+    @Override
+    @Transactional
+    public void clearManualAbsent(int examId) {
+        studentDao.clearManualAbsent(examId);
+    }
+
+    @Override
+    @Transactional
+    public void updateException(int id, boolean exception) {
+        studentDao.updateException(id, exception);
+    }
+
+    private Specification<ExamStudent> buildSpecification(final ExamStudentSearchQuery query) {
+        return new Specification<ExamStudent>() {
+
+            @Override
+            public Predicate toPredicate(Root<ExamStudent> root, CriteriaQuery<?> cQuery, CriteriaBuilder cb) {
+                List<Predicate> predicates = new LinkedList<Predicate>();
+                Expression<Double> evaluationItemSum = cb.sum(root.get("objectiveScore").as(Double.class),
+                        root.get("subjectiveScore").as(Double.class));
+                if (query.getStartScroe() != null) {
+                    Predicate predicate1 = cb.ge(evaluationItemSum, query.getStartScroe());
+                    Predicate predicate2 = cb.le(evaluationItemSum, query.getEndScroe());
+                    if (query.getStartScroe() == 0) {
+                        Predicate predicate = cb.or(cb.equal(root.get("absent"), true),
+                                cb.equal(root.get("breach"), true));
+                        Predicate predicate3 = cb.and(predicate1, predicate2);
+                        predicates.add(cb.or(predicate, predicate3));
+                    } else {
+                        predicates.add(cb.equal(root.get("absent"), false));
+                        predicates.add(cb.equal(root.get("breach"), false));
+                        predicates.add(cb.and(predicate1, predicate2));
+                    }
+                }
+                // if (query.getStartScroe()!=null && query.getEndScroe()!=null
+                // ) {
+                // predicates.add(cb.between(evaluationItemSum,
+                // query.getStartScroe(), query.getEndScroe()));
+                // }
+                if (query.getExamId() > 0) {
+                    predicates.add(cb.equal(root.get("examId"), query.getExamId()));
+                }
+                if (StringUtils.isNotBlank(query.getIds())) {
+                    String[] ids = query.getIds().split(",");
+                    List<String> list = new ArrayList<String>();
+                    for (String str : ids) {
+                        list.add(str);
+                    }
+                    predicates.add(root.get("id").in(list));
+                }
+                if (StringUtils.isNotBlank(query.getExamNumber())) {
+                    predicates.add(cb.equal(root.get("examNumber"), query.getExamNumber()));
+                } else if (StringUtils.isNotBlank(query.getExamNumberIn())) {
+                    String[] list = query.getExamNumberIn().split(",");
+                    if (list.length > 0) {
+                        Predicate[] sub = new Predicate[list.length];
+                        for (int i = 0; i < list.length; i++) {
+                            sub[i] = cb.equal(root.get("examNumber"), list[i]);
+                        }
+                        predicates.add(cb.or(sub));
+                    }
+                }
+                if (StringUtils.isNotBlank(query.getStudentCode())) {
+                    predicates.add(cb.equal(root.get("studentCode"), query.getStudentCode()));
+                }
+                if (StringUtils.isNotBlank(query.getSubjectCode())) {
+                    predicates.add(cb.equal(root.get("subjectCode"), query.getSubjectCode()));
+                }
+                if (StringUtils.isNotBlank(query.getCampusName())) {
+                    predicates.add(cb.equal(root.get("campusName"), query.getCampusName()));
+                }
+                if (StringUtils.isNotBlank(query.getName())) {
+                    predicates.add(cb.like(root.get("name").as(String.class), query.getName() + "%"));
+                }
+                if (StringUtils.isNotBlank(query.getBatchCode())) {
+                    predicates.add(cb.equal(root.get("batchCode"), query.getBatchCode()));
+                }
+                if (StringUtils.isNotBlank(query.getPackageCode())) {
+                    predicates.add(cb.equal(root.get("packageCode"), query.getPackageCode()));
+                }
+                if (StringUtils.isNotBlank(query.getSubjectLevel())) {
+                    predicates.add(cb.equal(root.get("subjectLevel"), query.getSubjectLevel()));
+                }
+                if (StringUtils.isNotBlank(query.getSubjectCategory())) {
+                    predicates.add(cb.equal(root.get("subjectCategory"), query.getSubjectCategory()));
+                }
+                if (query.getObjectiveScore() != null) {
+                    predicates.add(cb.equal(root.get("objectiveScore"), query.getObjectiveScore()));
+                } else if (query.getObjectiveScoreGt() != null) {
+                    predicates.add(
+                            cb.greaterThan(root.get("objectiveScore").as(Double.class), query.getObjectiveScoreGt()));
+                } else if (query.getObjectiveScoreLt() != null) {
+                    predicates
+                            .add(cb.lessThan(root.get("objectiveScore").as(Double.class), query.getObjectiveScoreLt()));
+                }
+                if (query.getSubjectiveScore() != null) {
+                    predicates.add(cb.equal(root.get("subjectiveScore"), query.getSubjectiveScore()));
+                } else if (query.getSubjectiveScoreGt() != null) {
+                    predicates.add(
+                            cb.greaterThan(root.get("subjectiveScore").as(Double.class), query.getSubjectiveScoreGt()));
+                } else if (query.getSubjectiveScoreLt() != null) {
+                    predicates.add(
+                            cb.lessThan(root.get("subjectiveScore").as(Double.class), query.getSubjectiveScoreLt()));
+                }
+                if (query.getUpload() != null) {
+                    predicates.add(cb.equal(root.get("upload"), query.getUpload()));
+                }
+                if (query.getAbsent() != null) {
+                    if (query.getAbsent()) {// 缺考=缺考+ 未上传
+                        predicates.add(cb.or(cb.equal(root.get("absent"), true), cb.equal(root.get("upload"), false)));
+                    } else {
+                        predicates.add(cb.equal(root.get("absent"), query.getAbsent()));
+                        predicates.add(cb.equal(root.get("upload"), true));
+                    }
+                }
+                if (query.getManualAbsent() != null) {
+                    predicates.add(cb.equal(root.get("manualAbsent"), query.getManualAbsent()));
+                }
+                if (query.getBreach() != null) {
+                    predicates.add(cb.equal(root.get("breach"), query.getBreach()));
+                }
+                if (query.getException() != null) {
+                    predicates.add(cb.equal(root.get("exception"), query.getException()));
+                }
+                if (StringUtils.isNotBlank(query.getSubjectCodeIn())) {
+                    String[] list = query.getSubjectCodeIn().split(",");
+                    if (list.length > 0) {
+                        Predicate[] sub = new Predicate[list.length];
+                        for (int i = 0; i < list.length; i++) {
+                            sub[i] = cb.equal(root.get("subjectCode"), list[i]);
+                        }
+                        predicates.add(cb.or(sub));
+                    }
+                }
+                if (StringUtils.isNotBlank(query.getCampusNameIn())) {
+                    String[] list = query.getCampusNameIn().split(",");
+                    if (list.length > 0) {
+                        Predicate[] sub = new Predicate[list.length];
+                        for (int i = 0; i < list.length; i++) {
+                            sub[i] = cb.equal(root.get("campusName"), list[i]);
+                        }
+                        predicates.add(cb.or(sub));
+                    }
+                }
+                if (StringUtils.isNotBlank(query.getSubjectCodeNotIn())) {
+                    String[] list = query.getSubjectCodeNotIn().split(",");
+                    if (list.length > 0) {
+                        Predicate[] sub = new Predicate[list.length];
+                        for (int i = 0; i < list.length; i++) {
+                            sub[i] = cb.notEqual(root.get("subjectCode"), list[i]);
+                        }
+                        predicates.add(cb.and(sub));
+                    }
+                }
+                if (StringUtils.isNotBlank(query.getCampusNameNotIn())) {
+                    String[] list = query.getCampusNameNotIn().split(",");
+                    if (list.length > 0) {
+                        Predicate[] sub = new Predicate[list.length];
+                        for (int i = 0; i < list.length; i++) {
+                            sub[i] = cb.notEqual(root.get("campusName"), list[i]);
+                        }
+                        predicates.add(cb.and(sub));
+                    }
+                }
+                if (query.getStudentId() != null) {
+                    predicates.add(cb.equal(root.get("id"), query.getStudentId()));
+                }
+                return predicates.isEmpty() ? cb.conjunction()
+                        : cb.and(predicates.toArray(new Predicate[predicates.size()]));
+            }
+        };
+
+    }
+
+    @Override
+    public ExamStudent findBySchoolIdAndSubjectCodeAndStudentCode(Integer schoolId, String subjectCode,
+            String studentCode) {
+        List<Exam> exams = examService.findBySchoolId(schoolId);
+        if (exams != null && exams.size() > 0) {
+            return studentDao.findByExamIdAndSubjectCodeAndStudentCode(exams.get(0).getId(), subjectCode, studentCode);
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public ExamStudent findBySchoolIdAndSubjectCodeAndStudentCodeAndRemark(Integer schoolId, String subjectCode,
+            String studentCode, String examSeqCode) {
+        return studentDao.findBySchoolIdAndSubjectCodeAndStudentCodeAndRemark(schoolId, subjectCode, studentCode,
+                examSeqCode);
+    }
+
+    @Override
+    public ExamStudent findByExamIdAndSubjectCodeAndStudentCode(Integer examId, String subjectCode,
+            String studentCode) {
+        return studentDao.findByExamIdAndSubjectCodeAndStudentCode(examId, subjectCode, studentCode);
+    }
+
+    @Override
+    public ExamStudent findByExamIdAndSchoolIdAndSubjectCodeAndStudentCode(Integer examId, Integer schoolId,
+            String subjectCode, String studentCode) {
+        return studentDao.findByExamIdAndSchoolIdAndSubjectCodeAndStudentCode(examId, schoolId, subjectCode,
+                studentCode);
+    }
+
+    @Override
+    public List<Object[]> statisticsByExamIdAndSubjectCode(Integer examId, String code, Boolean upload,
+            boolean absent) {
+        return studentDao.statisticsByExamIdAndSubjectCode(examId, code, upload, absent);
+    }
+
+    @Override
+    public List<Object[]> statisticsByExamIdAndSubjectCode(Integer examId, String code) {
+        return studentDao.statisticsByExamIdAndSubjectCode(examId, code);
+    }
+
+    @Override
+    public Long countByExamIdAndSubjectCodeAndCampus(Integer examId, String subjectCode, String campusName,
+            boolean upload, boolean absent) {
+        ExamStudentSearchQuery query = new ExamStudentSearchQuery();
+        query.setExamId(examId);
+        query.setSubjectCode(subjectCode);
+        query.setCampusName(campusName);
+        query.setUpload(upload);
+        query.setAbsent(absent);
+        return countByQuery(query);
+    }
+
+    @Override
+    public ExamStudent findBySchoolIdAndSubjectCodeStartingWithAndStudentCode(Integer schoolId, String subjectCode,
+            String studentCode) {
+        List<Exam> exams = examService.findBySchoolId(schoolId);
+        if (exams != null && exams.size() > 0) {
+            return studentDao.findByExamIdAndSubjectCodeStartingWithAndStudentCode(exams.get(0).getId(), subjectCode,
+                    studentCode);
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public String findIdsByMarkLogin(String markLogin) {
+        List<String[]> o = studentDao.findIdsByMarkLogin(markLogin);
+        return StringUtils.join(o, ",");
+    }
+
+    @Override
+    public String findIdsByMarkName(String MarkName) {
+        List<String[]> o = studentDao.findIdsByMarkName(MarkName);
+        return StringUtils.join(o, ",");
+    }
+
+    @Override
+    /**
+     * 根据缺考和违纪字段联合查询
+     */
+    public List<Object[]> statisticsByAbsentAndBreach(Integer examId, String code, Boolean upload, boolean absent,
+            boolean breach) {
+        return studentDao.statisticsByAbsentAndBreach(examId, code, upload, absent, breach);
+    }
+
+    /**
+     * 根据考生获得某张原图上的评卷标记和分数明细
+     */
+    @Override
+    public List<PictureTag> buildSheetTags(ExamStudent student, int index) {
+        List<PictureTag> list = new LinkedList<>();
+        // 第一页原图才需要显示总分明细
+        // if (index == 1) {
+        // list.add(buildScoreDetailTag(student));
+        // }
+        // 构造评卷标记信息
+        Exam exam = examService.findById(student.getExamId());
+        List<PictureConfigItem> sliceConfig = exam.getSliceConfigList();
+        if (!sliceConfig.isEmpty()) {
+            List<PictureTag> tags = PictureConfigTransform
+                    .process(sliceConfig, getSliceSet(student), getSliceTags(student)).get(index);
+            if (tags != null) {
+                list.addAll(tags);
+            }
+        }
+        return list;
+    }
+
+    /**
+     * 根据考生获得所有原图上的评卷标记和分数明细
+     */
+    @Override
+    public Map<Integer, List<PictureTag>> buildSheetTags(ExamStudent student) {
+        List<OriginTag> tags = new ArrayList<OriginTag>();
+        Exam exam = examService.findById(student.getExamId());
+        List<PictureConfigItem> sliceConfig = exam.getSliceConfigList();
+        if (!sliceConfig.isEmpty()) {
+            // 有裁切图配置时才需要获取原始评卷标记信息
+            tags = getSliceTags(student);
+        }
+        Map<Integer, List<PictureTag>> map = PictureConfigTransform.process(sliceConfig, getSliceSet(student), tags);
+        // List<PictureTag> list = map.get(1);
+        // if (list == null) {
+        // list = new LinkedList<>();
+        // map.put(1, list);
+        // }
+        // list.add(buildScoreDetailTag(student));
+        return map;
+    }
+
+    /**
+     * 根据考生获取所有评卷分组的评卷标记内容
+     */
+    @Override
+    public List<OriginTag> getSliceTags(ExamStudent student) {
+        List<ExamQuestion> questions = questionService.findByExamAndSubjectAndObjective(student.getExamId(),
+                student.getSubjectCode(), false);
+        List<ScoreItem> scoreList = student.getScoreList(false);
+        return buildOriginTags(student, questions, scoreList);
+    }
+
+    /**
+     * 根据考生构造显示到第一张原图左上角的总分与主客观题明细标记
+     * 
+     * @param student
+     * @return
+     */
+    private PictureTag buildScoreDetailTag(ExamStudent student) {
+        DecimalFormat format = new DecimalFormat("###.#");
+        // 所有显示内容
+        List<String> lines = new LinkedList<>();
+        lines.add("成绩明细");
+        // 总分得分明细
+        lines.add("总分 (客观+主观) | " + format.format(student.getTotalScore()) + "="
+                + format.format(student.getObjectiveScore() != null ? student.getObjectiveScore() : 0) + "+"
+                + format.format(student.getSubjectiveScore() != null ? student.getSubjectiveScore() : 0));
+        // 客观题得分明细
+        List<String> objectives = new LinkedList<>();
+        List<ExamQuestion> questions = questionService.findByExamAndSubjectAndObjective(student.getExamId(),
+                student.getSubjectCode(), true);
+        List<ScoreItem> scoreList = student.getScoreList(true);
+        List<String> details = new ArrayList<>();
+        int i = 0;
+        int length = 0;
+        for (ScoreItem item : scoreList) {
+            i++;
+            if (questions.size() < i) {
+                break;
+            }
+            ExamQuestion question = questions.get(i - 1);
+            if (question.getTotalScore() == null || question.getTotalScore() == 0) {
+                continue;
+            }
+            String content = item.getAnswer() + ":" + format.format(item.getScore());
+            if ((length + content.length()) > 60) {
+                // 一行只显示不超过60个字符,避免遮盖主观题答题区域
+                objectives.add(StringUtils.join(details, ","));
+                details.clear();
+                length = 0;
+            }
+            details.add(content);
+            length += content.length();
+        }
+        if (!details.isEmpty()) {
+            objectives.add(StringUtils.join(details, ","));
+            details.clear();
+        }
+        if (objectives.size() > 1) {
+            objectives.add(0, "客观题明细");
+            lines.addAll(objectives);
+        }
+        PictureTag tag = new PictureTag();
+        tag.setSize(30);
+        tag.setTop(40);
+        tag.setContent(lines);
+        return tag;
+    }
+
+    /**
+     * 获取某个考生某个评卷分组的所有评卷标记
+     * 
+     * @param student
+     * @param group
+     * @param questions
+     * @param scoreList
+     * @return
+     */
+    private List<OriginTag> buildOriginTags(ExamStudent student, List<ExamQuestion> questions,
+            List<ScoreItem> scoreList) {
+        DecimalFormat format = new DecimalFormat("###.#");
+        // 从考生主观题得分中拆解出本大题得分
+        // double score = 0;
+        // List<Double> details = new ArrayList<>();
+        // int i = -1;
+        // for (ExamQuestion question : questions) {
+        // i++;
+        // double value = scoreList.size() > i ? scoreList.get(i).getScore() :
+        // 0;
+        // score += value;
+        // details.add(value);
+        // }
+        // 原图中需要显示的内容列表
+        List<OriginTag> originTags = new LinkedList<>();
+        // 首先添加本大题总得分
+        // originTags.add(new OriginTag(format.format(score), 0, 0));
+        // 检测应该使用哪个评卷任务的轨迹记录
+        MarkLibrary library = libraryService.findByStudentId(student.getId());
+        if (library != null) {
+            // 添加轨迹分
+            List<MarkTrack> tracks = trackService.findByStudentId(student.getId());
+            for (MarkTrack markTrack : tracks) {
+                originTags.add(new OriginTag(format.format(markTrack.getScore()), markTrack.getPositionX(),
+                        markTrack.getPositionY()));
+            }
+            // 添加特殊标记
+            List<MarkSpecialTag> specialTags = specialTagService.findByLibraryId(library.getId());
+            for (MarkSpecialTag markSpecialTag : specialTags) {
+                originTags.add(new OriginTag(markSpecialTag.getTagName(), markSpecialTag.getPositionX(),
+                        markSpecialTag.getPositionY()));
+            }
+        }
+        return originTags;
+    }
+
+    private Set<Integer> getSliceSet(ExamStudent student) {
+        Set<Integer> set = new HashSet<>();
+        MarkLibrary library = libraryService.findByStudentId(student.getId());
+        if (library != null && library.getPicStart() != null && library.getPicCount() != null) {
+            for (int i = 1; i <= library.getPicCount(); i++) {
+                if (i >= library.getPicStart()) {
+                    set.add(i);
+                }
+            }
+        }
+        return set;
+    }
+}

+ 131 - 0
stmms-biz/src/main/java/cn/com/qmth/stmms/biz/mark/model/PictureConfigItem.java

@@ -0,0 +1,131 @@
+package cn.com.qmth.stmms.biz.mark.model;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * 单个裁切区设置模型
+ * 
+ * @author luoshi
+ *
+ */
+public class PictureConfigItem {
+
+    // 数据库中多个单元紧凑保存的拼接符,兼容旧版本只保存图片序号的模式
+    public static final String DB_ITEM_JOINER = ",";
+
+    // 数据库中单个单元紧凑保存的拼接符,兼容旧版本只保存图片序号的模式
+    public static final String DB_FIELD_JOINER = ":";
+
+    // 裁切去所属的图片序号,从1开始
+    private int i;
+
+    // 裁切区左上角X坐标
+    private int x;
+
+    // 裁切区左上角Y坐标
+    private int y;
+
+    // 裁切区宽度
+    private int w;
+
+    // 裁切区高度
+    private int h;
+
+    public PictureConfigItem() {
+
+    }
+
+    public PictureConfigItem(String text) {
+        text = StringUtils.trimToEmpty(text);
+        String[] values = StringUtils.split(text, DB_FIELD_JOINER);
+        try {
+            i = Integer.valueOf(values[0]);
+            if (values.length > 1) {
+                x = Integer.valueOf(values[1]);
+            }
+            if (values.length > 2) {
+                y = Integer.valueOf(values[2]);
+            }
+            if (values.length > 3) {
+                w = Integer.valueOf(values[3]);
+            }
+            if (values.length > 4) {
+                h = Integer.valueOf(values[4]);
+            }
+        } catch (Exception e) {
+            throw new IllegalArgumentException("Invalid PictureConfigItem init text:" + text);
+        }
+    }
+
+    public int getI() {
+        return i;
+    }
+
+    public void setI(int i) {
+        this.i = i;
+    }
+
+    public int getX() {
+        return x;
+    }
+
+    public void setX(int x) {
+        this.x = x;
+    }
+
+    public int getY() {
+        return y;
+    }
+
+    public void setY(int y) {
+        this.y = y;
+    }
+
+    public int getW() {
+        return w;
+    }
+
+    public void setW(int w) {
+        this.w = w;
+    }
+
+    public int getH() {
+        return h;
+    }
+
+    public void setH(int h) {
+        this.h = h;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append(i).append(DB_FIELD_JOINER);
+        sb.append(x).append(DB_FIELD_JOINER);
+        sb.append(y).append(DB_FIELD_JOINER);
+        sb.append(w).append(DB_FIELD_JOINER);
+        sb.append(h);
+        return sb.toString();
+    }
+
+    public static List<PictureConfigItem> parse(String text) {
+        List<PictureConfigItem> list = new LinkedList<PictureConfigItem>();
+        text = StringUtils.trimToEmpty(text);
+        String[] values = StringUtils.split(text, DB_ITEM_JOINER);
+        for (String value : values) {
+            try {
+                PictureConfigItem item = new PictureConfigItem(value);
+                if (item != null && item.i > 0) {
+                    list.add(item);
+                }
+            } catch (Exception e) {
+                continue;
+            }
+        }
+        return list;
+    }
+
+}

+ 45 - 0
stmms-biz/src/main/java/cn/com/qmth/stmms/biz/utils/OriginTag.java

@@ -0,0 +1,45 @@
+package cn.com.qmth.stmms.biz.utils;
+
+public class OriginTag {
+
+    private String content;
+
+    private double positionX;
+
+    private double positionY;
+
+    public OriginTag() {
+
+    }
+
+    public OriginTag(String content, double positionX, double positionY) {
+        this.content = content;
+        this.positionX = positionX;
+        this.positionY = positionY;
+    }
+
+    public String getContent() {
+        return content;
+    }
+
+    public void setContent(String content) {
+        this.content = content;
+    }
+
+    public double getPositionX() {
+        return positionX;
+    }
+
+    public void setPositionX(double positionX) {
+        this.positionX = positionX;
+    }
+
+    public double getPositionY() {
+        return positionY;
+    }
+
+    public void setPositionY(double positionY) {
+        this.positionY = positionY;
+    }
+
+}

+ 105 - 0
stmms-biz/src/main/java/cn/com/qmth/stmms/biz/utils/PictureConfigTransform.java

@@ -0,0 +1,105 @@
+package cn.com.qmth.stmms.biz.utils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import cn.com.qmth.stmms.biz.mark.model.PictureConfigItem;
+
+/**
+ * 根据裁切图配置与评卷图片拼接配置,将各显示元素坐标转换为原图坐标
+ * 
+ * @author luoshi
+ *
+ */
+public class PictureConfigTransform {
+
+    /**
+     * 转换方法
+     * 
+     * @param sliceConfigs
+     *            - 裁切图配置
+     * @param sliceSet
+     *            - 实际裁切图序号
+     * @param tags
+     *            - 原始标记内容
+     * @return 原图序号与新的显示元素列表,序号从1开始
+     */
+    public static Map<Integer, List<PictureTag>> process(List<PictureConfigItem> sliceConfigs, Set<Integer> sliceSet,
+            List<OriginTag> tags) {
+        Map<Integer, List<PictureTag>> map = new HashMap<>();
+        if (sliceConfigs.isEmpty()) {
+            return map;
+        }
+        if (sliceSet.isEmpty() || tags.isEmpty()) {
+            return map;
+        }
+
+        int maxWidth = 0;
+        int totalHeight = 0;
+        int index = 0;
+        List<PictureConfigItem> slices = new ArrayList<PictureConfigItem>();
+        // 计算评卷分组拼接图的总高度与最大宽度
+        for (PictureConfigItem config : sliceConfigs) {
+            index++;
+            if (!sliceSet.contains(index)) {
+                continue;
+            }
+            // 直接使用裁切图的宽高
+            maxWidth = Math.max(maxWidth, config.getW());
+            totalHeight += config.getH();
+            slices.add(config);
+        }
+        // 遍历所有显示元素
+        for (OriginTag tag : tags) {
+            // 计算显示元素在拼接图内的绝对位置
+            int left = (int) (maxWidth * tag.getPositionX());
+            int top = (int) (totalHeight * tag.getPositionY());
+            int start = 0;
+            for (PictureConfigItem config : slices) {
+                if (top <= (config.getH() + start)) {
+                    // 根据绝对高度判断显示元素是否落在当前拼接块
+                    buildTag(map, config, tag.getContent(), left, top - start);
+                    break;
+                } else {
+                    start += config.getH();
+                }
+            }
+        }
+        return map;
+    }
+
+    /**
+     * 根据计算出的拼接块,构造新的原图显示元素
+     * 
+     * @param tags
+     *            - 原图显示元素集合
+     * @param config
+     *            - 当前裁切图配置
+     * @param content
+     *            - 显示元素内容
+     * @param left
+     *            - 在拼接块内的左偏移
+     * @param top
+     *            - 在拼接块内的上偏移
+     */
+    private static void buildTag(Map<Integer, List<PictureTag>> tags, PictureConfigItem config, String content,
+            int left, int top) {
+        PictureTag tag = new PictureTag();
+        tag.setContent(content);
+        // 原图内偏移=拼接块内偏移+拼接块在裁切图内偏移+裁切图在原图内偏移
+        tag.setLeft(left + config.getX());
+        tag.setTop(top + config.getY());
+
+        List<PictureTag> list = tags.get(config.getI());
+        if (list == null) {
+            list = new LinkedList<>();
+            tags.put(config.getI(), list);
+        }
+        list.add(tag);
+    }
+
+}

+ 53 - 0
stmms-biz/src/main/java/cn/com/qmth/stmms/biz/utils/PictureTag.java

@@ -0,0 +1,53 @@
+package cn.com.qmth.stmms.biz.utils;
+
+import java.util.List;
+
+public class PictureTag {
+
+    private String[] content;
+
+    private int left;
+
+    private int top;
+
+    private int size;
+
+    public String[] getContent() {
+        return content;
+    }
+
+    public void setContent(String... content) {
+        this.content = content;
+    }
+
+    public void setContent(List<String> content) {
+        if (content != null && !content.isEmpty()) {
+            this.content = content.toArray(new String[content.size()]);
+        }
+    }
+
+    public int getLeft() {
+        return left;
+    }
+
+    public void setLeft(int left) {
+        this.left = left;
+    }
+
+    public int getTop() {
+        return top;
+    }
+
+    public void setTop(int top) {
+        this.top = top;
+    }
+
+    public int getSize() {
+        return size;
+    }
+
+    public void setSize(int size) {
+        this.size = size;
+    }
+
+}

+ 1322 - 1323
stmms-common/src/main/java/cn/com/qmth/stmms/common/upyun/UpYun.java

@@ -1,1323 +1,1322 @@
-package cn.com.qmth.stmms.common.upyun;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.io.UnsupportedEncodingException;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.TimeZone;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-public class UpYun {
-
-    private static Logger log = LoggerFactory.getLogger(UpYun.class);
-
-    /** 默认的编码格式 */
-    public static final String DEFAULT_CHARSET = "UTF-8";
-
-    /** SKD版本号 */
-    private final String VERSION = "2.0";
-
-    /** 路径的分割符 */
-    private final String SEPARATOR = "/";
-
-    private final String AUTHORIZATION = "Authorization";
-
-    private final String DATE = "Date";
-
-    private final String CONTENT_LENGTH = "Content-Length";
-
-    public static final String CONTENT_MD5 = "Content-MD5";
-
-    private final String CONTENT_SECRET = "Content-Secret";
-
-    private final String MKDIR = "mkdir";
-
-    private final String X_UPYUN_WIDTH = "x-upyun-width";
-
-    private final String X_UPYUN_HEIGHT = "x-upyun-height";
-
-    private final String X_UPYUN_FRAMES = "x-upyun-frames";
-
-    public static final String X_UPYUN_FILE_TYPE = "x-upyun-file-type";
-
-    public static final String X_UPYUN_FILE_SIZE = "x-upyun-file-size";
-
-    public static final String X_UPYUN_FILE_DATE = "x-upyun-file-date";
-
-    private final String METHOD_HEAD = "HEAD";
-
-    private final String METHOD_GET = "GET";
-
-    private final String METHOD_PUT = "PUT";
-
-    private final String METHOD_DELETE = "DELETE";
-
-    /**
-     * 根据网络条件自动选择接入点:v0.api.upyun.com
-     */
-    public static final String ED_AUTO = "v0.api.upyun.com";
-
-    /**
-     * 电信接入点:v1.api.upyun.com
-     */
-    public static final String ED_TELECOM = "v1.api.upyun.com";
-
-    /**
-     * 联通网通接入点:v2.api.upyun.com
-     */
-    public static final String ED_CNC = "v2.api.upyun.com";
-
-    /**
-     * 移动铁通接入点:v3.api.upyun.com
-     */
-    public static final String ED_CTT = "v3.api.upyun.com";
-
-    // 默认不开启debug模式
-    public boolean debug = false;
-
-    // 默认的超时时间:30秒
-    private int timeout = 30 * 1000;
-
-    // 默认为自动识别接入点
-    private String apiDomain = ED_AUTO;
-
-    // 待上传文件的 Content-MD5 值
-    private String contentMD5 = null;
-
-    // 待上传文件的"访问密钥"
-    private String fileSecret = null;
-
-    // 空间名
-    protected String bucketName = null;
-
-    // 操作员名
-    protected String userName = null;
-
-    // 操作员密码
-    protected String password = null;
-
-    // 图片信息的参数
-    protected String picWidth = null;
-
-    protected String picHeight = null;
-
-    protected String picFrames = null;
-
-    protected String picType = null;
-
-    // 文件信息的参数
-    protected String fileType = null;
-
-    protected String fileSize = null;
-
-    protected String fileDate = null;
-
-    /**
-     * 初始化 UpYun 存储接口
-     * 
-     * @param bucketName
-     *            空间名称
-     * @param userName
-     *            操作员名称
-     * @param password
-     *            密码,不需要MD5加密
-     * @return UpYun object
-     */
-    public UpYun(String bucketName, String userName, String password) {
-        this.bucketName = bucketName;
-        this.userName = userName;
-        this.password = md5(password);
-    }
-
-    /**
-     * 切换 API 接口的域名接入点
-     * <p>
-     * 可选参数:<br>
-     * 1) UpYun.ED_AUTO(v0.api.upyun.com):默认,根据网络条件自动选择接入点 <br>
-     * 2) UpYun.ED_TELECOM(v1.api.upyun.com):电信接入点<br>
-     * 3) UpYun.ED_CNC(v2.api.upyun.com):联通网通接入点<br>
-     * 4) UpYun.ED_CTT(v3.api.upyun.com):移动铁通接入点
-     * 
-     * @param domain
-     *            域名接入点
-     */
-    public void setApiDomain(String domain) {
-        this.apiDomain = domain;
-    }
-
-    /**
-     * 查看当前的域名接入点
-     * 
-     * @return
-     */
-    public String getApiDomain() {
-        return apiDomain;
-    }
-
-    /**
-     * 设置连接超时时间,默认为30秒
-     * 
-     * @param second
-     *            秒数,60即为一分钟超时
-     */
-    public void setTimeout(int second) {
-        this.timeout = second * 1000;
-    }
-
-    /**
-     * 查看当前的超时时间
-     * 
-     * @return
-     */
-    public int getTimeout() {
-        return timeout;
-    }
-
-    /**
-     * 查看当前是否是debug模式
-     * 
-     * @return
-     */
-    public boolean isDebug() {
-        return debug;
-    }
-
-    /**
-     * 设置是否开启debug模式
-     * 
-     * @param debug
-     */
-    public void setDebug(boolean debug) {
-        this.debug = debug;
-    }
-
-    /**
-     * 设置待上传文件的 Content-MD5 值
-     * <p>
-     * 如果又拍云服务端收到的文件MD5值与用户设置的不一致,将回报 406 Not Acceptable 错误
-     * 
-     * @param md5Value
-     *            文件 MD5 校验后的内容
-     */
-    public void setContentMD5(String md5Value) {
-        this.contentMD5 = md5Value;
-    }
-
-    /**
-     * 设置待上传文件的"访问密钥"
-     * <p>
-     * 注意:<br>
-     * 仅支持图片空!设置密钥后,无法根据原文件URL直接访问,需带 URL 后面加上 (缩略图间隔标志符+密钥) 进行访问
-     * <p>
-     * 举例:<br>
-     * 如果缩略图间隔标志符为"!",密钥为"bac",上传文件路径为"/folder/test.jpg",<br>
-     * 那么该图片的对外访问地址为:http://空间域名 /folder/test.jpg!bac
-     * 
-     * @param secret
-     *            密钥字符串
-     */
-    public void setFileSecret(String secret) {
-        this.fileSecret = secret;
-    }
-
-    public String getPicWidth() {
-        return picWidth;
-    }
-
-    public String getPicHeight() {
-        return picHeight;
-    }
-
-    public String getPicFrames() {
-        return picFrames;
-    }
-
-    public String getPicType() {
-        return picType;
-    }
-
-    /**
-     * 获取当前SDK的版本号
-     * 
-     * @return SDK版本号
-     */
-    public String version() {
-        return VERSION;
-    }
-
-    /**
-     * 获取总体空间的占用量
-     * 
-     * @param path
-     *            目标路径
-     * @return 空间占用量,失败时返回 -1
-     */
-    public long getBucketUsage() {
-        return getFolderUsage("/");
-    }
-
-    /**
-     * 获取某个子目录的占用量
-     * 
-     * @param path
-     *            目标路径
-     * @return 空间占用量,失败时返回 -1
-     */
-    public long getFolderUsage(String path) {
-
-        long usage = -1;
-
-        String result = HttpAction(METHOD_GET, formatPath(path) + "/?usage");
-
-        if (!isEmpty(result)) {
-
-            try {
-                usage = Long.parseLong(result.trim());
-            } catch (NumberFormatException e) {
-            }
-        }
-
-        return usage;
-    }
-
-    /**
-     * 上传文件
-     * 
-     * @param filePath
-     *            文件路径(包含文件名)
-     * @param datas
-     *            文件内容
-     * 
-     * @return true or false
-     */
-    public boolean writeFile(String filePath, byte[] datas) {
-        return writeFile(filePath, datas, false, null);
-    }
-
-    /**
-     * 上传文件
-     * 
-     * @param filePath
-     *            文件路径(包含文件名)
-     * @param datas
-     *            文件内容
-     * @param auto
-     *            是否自动创建父级目录(最多10级)
-     * 
-     * @return true or false
-     */
-    public boolean writeFile(String filePath, byte[] datas, boolean auto) {
-        return writeFile(filePath, datas, auto, null);
-    }
-
-    /**
-     * 上传文件
-     * 
-     * @param filePath
-     *            文件路径(包含文件名)
-     * @param datas
-     *            文件内容
-     * @param auto
-     *            是否自动创建父级目录(最多10级)
-     * @param params
-     *            额外参数
-     * 
-     * @return true or false
-     */
-    public boolean writeFile(String filePath, byte[] datas, boolean auto, Map<String, String> params) {
-
-        return HttpAction(METHOD_PUT, formatPath(filePath), datas, null, auto, params) != null;
-    }
-
-    /**
-     * 上传文件
-     * 
-     * @param filePath
-     *            文件路径(包含文件名)
-     * @param String
-     *            datas 文件内容
-     * 
-     * @return true or false
-     */
-    public boolean writeFile(String filePath, String datas) {
-        return writeFile(filePath, datas, false, null);
-    }
-
-    /**
-     * 上传文件
-     * 
-     * @param filePath
-     *            文件路径(包含文件名)
-     * @param String
-     *            datas 文件内容
-     * @param auto
-     *            是否自动创建父级目录(最多10级)
-     * 
-     * @return true or false
-     */
-    public boolean writeFile(String filePath, String datas, boolean auto) {
-        return writeFile(filePath, datas, auto, null);
-    }
-
-    /**
-     * 上传文件
-     * 
-     * @param filePath
-     *            文件路径(包含文件名)
-     * @param String
-     *            datas 文件内容
-     * @param auto
-     *            是否自动创建父级目录(最多10级)
-     * @param params
-     *            额外参数
-     * 
-     * @return true or false
-     */
-    public boolean writeFile(String filePath, String datas, boolean auto, Map<String, String> params) {
-
-        boolean result = false;
-
-        try {
-            result = writeFile(filePath, datas.getBytes(DEFAULT_CHARSET), auto, params);
-        } catch (Exception e) {
-            e.printStackTrace();
-        }
-
-        return result;
-    }
-
-    /**
-     * 上传文件
-     * 
-     * @param filePath
-     *            文件路径(包含文件名)
-     * @param file
-     *            待上传的文件
-     * 
-     * @return true or false
-     * @throws IOException
-     */
-    public boolean writeFile(String filePath, File file) throws IOException {
-        return writeFile(filePath, file, false, null);
-    }
-
-    /**
-     * 上传文件
-     * 
-     * @param filePath
-     *            文件路径(包含文件名)
-     * @param file
-     *            待上传的文件
-     * @param auto
-     *            是否自动创建父级目录(最多10级)
-     * 
-     * @return true or false
-     * @throws IOException
-     */
-    public boolean writeFile(String filePath, File file, boolean auto) throws IOException {
-        return writeFile(filePath, file, auto, null);
-    }
-
-    /**
-     * 上传文件
-     * 
-     * @param filePath
-     *            文件路径(包含文件名)
-     * @param file
-     *            待上传的文件
-     * @param auto
-     *            是否自动创建父级目录(最多10级)
-     * @param params
-     *            额外参数
-     * 
-     * @return true or false
-     * @throws IOException
-     */
-    public boolean writeFile(String filePath, File file, boolean auto, Map<String, String> params) throws IOException {
-
-        filePath = formatPath(filePath);
-
-        InputStream is = null;
-        OutputStream os = null;
-        HttpURLConnection conn = null;
-
-        try {
-            // 读取待上传的文件
-            is = new FileInputStream(file);
-
-            // 获取链接
-            URL url = new URL("http://" + apiDomain + filePath);
-            conn = (HttpURLConnection) url.openConnection();
-
-            // 设置必要参数
-            conn.setConnectTimeout(timeout);
-            conn.setRequestMethod(METHOD_PUT);
-            conn.setUseCaches(false);
-            conn.setDoOutput(true);
-
-            // 设置时间
-            conn.setRequestProperty(DATE, getGMTDate());
-            // 设置签名
-            conn.setRequestProperty(AUTHORIZATION, sign(conn, filePath, is.available()));
-
-            // 设置文件的 MD5 参数
-            if (!isEmpty(this.contentMD5)) {
-                conn.setRequestProperty(CONTENT_MD5, this.contentMD5);
-                this.contentMD5 = null;
-            }
-
-            // 设置文件的访问密钥
-            if (!isEmpty(this.fileSecret)) {
-                conn.setRequestProperty(CONTENT_SECRET, this.fileSecret);
-                this.fileSecret = null;
-            }
-
-            // 是否自动创建父级目录
-            if (auto) {
-                conn.setRequestProperty(MKDIR, "true");
-            }
-
-            // 设置额外的参数,如图片缩略图等
-            if (params != null && !params.isEmpty()) {
-
-                for (Map.Entry<String, String> param : params.entrySet()) {
-                    conn.setRequestProperty(param.getKey(), param.getValue());
-                }
-            }
-
-            // 创建链接
-            conn.connect();
-
-            os = conn.getOutputStream();
-            byte[] data = new byte[4096];
-            int temp = 0;
-
-            // 上传文件内容
-            while ((temp = is.read(data)) != -1) {
-                os.write(data, 0, temp);
-            }
-
-            // 获取返回的信息
-            getText(conn, false);
-
-            // 上传成功
-            return true;
-
-        } catch (IOException e) {
-            if (debug)
-                e.printStackTrace();
-
-            // 上传失败
-            return false;
-
-        } finally {
-
-            if (os != null) {
-                os.close();
-                os = null;
-            }
-            if (is != null) {
-                is.close();
-                is = null;
-            }
-            if (conn != null) {
-                conn.disconnect();
-                conn = null;
-            }
-        }
-    }
-
-    /**
-     * 读取文件
-     * 
-     * @param filePath
-     *            文件路径(包含文件名)
-     * 
-     * @return 文件内容 或 null
-     */
-    public String readFile(String filePath) {
-        return HttpAction(METHOD_GET, formatPath(filePath));
-    }
-
-    /**
-     * 读取文件
-     * 
-     * @param filePath
-     *            文件路径(包含文件名)
-     * @param file
-     *            临时文件
-     * 
-     * @return true or false
-     */
-    public boolean readFile(String filePath, File file) {
-
-        String result = HttpAction(METHOD_GET, formatPath(filePath), null, file, false);
-
-        return "".equals(result);
-    }
-
-    /**
-     * 获取文件信息
-     * 
-     * @param filePath
-     *            文件路径(包含文件名)
-     * 
-     * @return 文件信息 或 null
-     */
-    public Map<String, String> getFileInfo(String filePath) {
-
-        HttpAction(METHOD_HEAD, formatPath(filePath));
-
-        // 判断是否存在文件信息
-        if (isEmpty(fileType) && isEmpty(fileSize) && isEmpty(fileDate)) {
-            return null;
-        }
-
-        Map<String, String> mp = new HashMap<String, String>();
-        mp.put("type", fileType);
-        mp.put("size", fileSize);
-        mp.put("date", fileDate);
-
-        return mp;
-    }
-
-    /**
-     * 删除文件
-     * 
-     * @param filePath
-     *            文件路径(包含文件名)
-     * 
-     * @return true or false
-     */
-    public boolean deleteFile(String filePath) {
-
-        return HttpAction(METHOD_DELETE, formatPath(filePath)) != null;
-    }
-
-    /**
-     * 创建目录
-     * 
-     * @param path
-     *            目录路径
-     * 
-     * @return true or false
-     */
-    public boolean mkDir(String path) {
-        return mkDir(path, false);
-    }
-
-    /**
-     * 创建目录
-     * 
-     * @param path
-     *            目录路径
-     * @param auto
-     *            是否自动创建父级目录(最多10级)
-     * 
-     * @return true or false
-     */
-    public boolean mkDir(String path, boolean auto) {
-
-        Map<String, String> params = new HashMap<String, String>(1);
-        params.put(PARAMS.KEY_MAKE_DIR.getValue(), "true");
-
-        String result = HttpAction(METHOD_PUT, formatPath(path), null, null, auto, params);
-
-        return result != null;
-    }
-
-    /**
-     * 读取目录列表
-     * 
-     * @param path
-     *            目录路径
-     * 
-     * @return List<FolderItem> 或 null
-     */
-    public List<FolderItem> readDir(String path) {
-
-        String result = HttpAction(METHOD_GET, formatPath(path) + SEPARATOR);
-
-        if (isEmpty(result))
-            return null;
-
-        List<FolderItem> list = new LinkedList<FolderItem>();
-
-        String[] datas = result.split("\n");
-
-        for (int i = 0; i < datas.length; i++) {
-            if (datas[i].indexOf("\t") > 0) {
-                list.add(new FolderItem(datas[i]));
-            }
-        }
-        return list;
-    }
-
-    /**
-     * 删除目录
-     * 
-     * @param path
-     *            目录路径
-     * 
-     * @return true or false
-     */
-    public boolean rmDir(String path) {
-        return HttpAction(METHOD_DELETE, formatPath(path)) != null;
-    }
-
-    /**
-     * 获取上传文件后的信息(仅图片空间有返回数据)
-     * 
-     * @param key
-     *            信息字段名(x-upyun-width、x-upyun-height、x-upyun-frames、x-upyun-file
-     *            -type)
-     * 
-     * @return value or NULL
-     * @deprecated
-     */
-    public String getWritedFileInfo(String key) {
-
-        if (isEmpty(picWidth))
-            return null;
-
-        if (X_UPYUN_WIDTH.equals(key))
-            return picWidth;
-        if (X_UPYUN_HEIGHT.equals(key))
-            return picHeight;
-        if (X_UPYUN_FRAMES.equals(key))
-            return picFrames;
-        if (X_UPYUN_FILE_TYPE.equals(key))
-            return picType;
-
-        return null;
-    }
-
-    /**
-     * 对字符串进行 MD5 加密
-     * 
-     * @param str
-     *            待加密字符串
-     * 
-     * @return 加密后字符串
-     */
-    public static String md5(String str) {
-        char hexDigits[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
-        MessageDigest md5 = null;
-        try {
-            md5 = MessageDigest.getInstance("MD5");
-            md5.update(str.getBytes(DEFAULT_CHARSET));
-        } catch (NoSuchAlgorithmException e) {
-            e.printStackTrace();
-            throw new RuntimeException(e.getMessage());
-        } catch (UnsupportedEncodingException e) {
-            e.printStackTrace();
-            throw new RuntimeException(e.getMessage());
-        }
-        byte[] encodedValue = md5.digest();
-        int j = encodedValue.length;
-        char finalValue[] = new char[j * 2];
-        int k = 0;
-        for (int i = 0; i < j; i++) {
-            byte encoded = encodedValue[i];
-            finalValue[k++] = hexDigits[encoded >> 4 & 0xf];
-            finalValue[k++] = hexDigits[encoded & 0xf];
-        }
-
-        return new String(finalValue);
-    }
-
-    /**
-     * 对文件进行 MD5 加密
-     * 
-     * @param file
-     *            待加密的文件
-     * 
-     * @return 文件加密后的 MD5 值
-     * @throws IOException
-     */
-    public static String md5(File file) throws IOException {
-        FileInputStream is = new FileInputStream(file);
-        char hexDigits[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
-        MessageDigest md5 = null;
-        try {
-            md5 = MessageDigest.getInstance("MD5");
-            int n = 0;
-            byte[] buffer = new byte[1024];
-            do {
-                n = is.read(buffer);
-                if (n > 0) {
-                    md5.update(buffer, 0, n);
-                }
-            } while (n != -1);
-            is.skip(0);
-        } catch (NoSuchAlgorithmException e) {
-            e.printStackTrace();
-            throw new RuntimeException(e.getMessage());
-        } finally {
-            is.close();
-        }
-
-        byte[] encodedValue = md5.digest();
-
-        int j = encodedValue.length;
-        char finalValue[] = new char[j * 2];
-        int k = 0;
-        for (int i = 0; i < j; i++) {
-            byte encoded = encodedValue[i];
-            finalValue[k++] = hexDigits[encoded >> 4 & 0xf];
-            finalValue[k++] = hexDigits[encoded & 0xf];
-        }
-
-        return new String(finalValue);
-    }
-
-    /**
-     * 获取 GMT 格式时间戳
-     * 
-     * @return GMT 格式时间戳
-     */
-    private String getGMTDate() {
-        SimpleDateFormat formater = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
-        formater.setTimeZone(TimeZone.getTimeZone("GMT"));
-        return formater.format(new Date());
-    }
-
-    /**
-     * 计算签名
-     * 
-     * @param conn
-     *            连接
-     * @param uri
-     *            请求地址
-     * @param length
-     *            请求所发Body数据长度
-     * 
-     * @return 签名字符串
-     */
-    private String sign(HttpURLConnection conn, String uri, long length) {
-        String sign = conn.getRequestMethod() + "&" + uri + "&" + conn.getRequestProperty(DATE) + "&" + length + "&"
-                + password;
-        return "UpYun " + userName + ":" + md5(sign);
-    }
-
-    /**
-     * 连接处理逻辑
-     * 
-     * @param method
-     *            请求方式 {GET, POST, PUT, DELETE}
-     * @param uri
-     *            请求地址
-     * 
-     * @return 请求结果(字符串)或 null
-     */
-    private String HttpAction(String method, String uri) {
-        return HttpAction(method, uri, null, null, false);
-    }
-
-    /**
-     * 连接处理逻辑
-     * 
-     * @param method
-     *            请求方式 {GET, POST, PUT, DELETE}
-     * @param uri
-     *            请求地址
-     * @param datas
-     *            该请求所需发送数据(可为 null)
-     * @param outFile
-     *            文件描述符(可为 null)
-     * @param auto
-     *            自动创建父级目录(最多10级)
-     * 
-     * @return 请求结果(字符串)或 null
-     */
-    private String HttpAction(String method, String uri, byte[] datas, File outFile, boolean auto) {
-
-        return HttpAction(method, uri, datas, outFile, auto, null);
-    }
-
-    /**
-     * 连接处理逻辑
-     * 
-     * @param method
-     *            请求方式 {GET, POST, PUT, DELETE}
-     * @param uri
-     *            请求地址
-     * @param datas
-     *            该请求所需发送数据(可为 null)
-     * @param outFile
-     *            文件描述符(可为 null)
-     * @param auto
-     *            自动创建父级目录(最多10级)
-     * @param params
-     *            额外参数
-     * 
-     * @return 请求结果(字符串)或 null
-     */
-    @SuppressWarnings("resource")
-    private String HttpAction(String method, String uri, byte[] datas, File outFile, boolean auto,
-            Map<String, String> params) {
-        String result = null;
-
-        HttpURLConnection conn = null;
-        OutputStream os = null;
-        InputStream is = null;
-
-        try {
-            // 获取链接
-            URL url = new URL("http://" + apiDomain + uri);
-            conn = (HttpURLConnection) url.openConnection();
-
-            // 设置必要参数
-            conn.setConnectTimeout(timeout);
-            conn.setRequestMethod(method);
-            conn.setUseCaches(false);
-            conn.setDoOutput(true);
-
-            // 设置时间
-            conn.setRequestProperty(DATE, getGMTDate());
-
-            // 是否自动创建父级目录
-            if (auto) {
-                conn.setRequestProperty(MKDIR, "true");
-            }
-
-            long contentLength = 0;
-            if (datas == null)
-                conn.setRequestProperty(CONTENT_LENGTH, "0");
-            else {
-                contentLength = datas.length;
-                conn.setRequestProperty(CONTENT_LENGTH, String.valueOf(datas.length));
-
-                // 设置文件的 MD5 参数
-                if (!isEmpty(this.contentMD5)) {
-                    conn.setRequestProperty(CONTENT_MD5, this.contentMD5);
-                    this.contentMD5 = null;
-                }
-                // 设置文件的访问密钥
-                if (!isEmpty(this.fileSecret)) {
-                    conn.setRequestProperty(CONTENT_SECRET, this.fileSecret);
-                    this.fileSecret = null;
-                }
-            }
-
-            // 设置签名
-            conn.setRequestProperty(AUTHORIZATION, sign(conn, uri, contentLength));
-
-            // 是否是创建文件目录
-            boolean isFolder = false;
-
-            // 设置额外的参数,如图片缩略图等
-            if (params != null && !params.isEmpty()) {
-
-                isFolder = !isEmpty(params.get(PARAMS.KEY_MAKE_DIR.getValue()));
-
-                for (Map.Entry<String, String> param : params.entrySet()) {
-                    conn.setRequestProperty(param.getKey(), param.getValue());
-                }
-            }
-
-            // 创建链接
-            conn.connect();
-
-            if (datas != null) {
-                os = conn.getOutputStream();
-                os.write(datas);
-                os.flush();
-            }
-
-            if (isFolder) {
-                os = conn.getOutputStream();
-                os.flush();
-            }
-
-            if (outFile == null) {
-
-                result = getText(conn, METHOD_HEAD.equals(method));
-
-            } else {
-                result = "";
-
-                os = new FileOutputStream(outFile);
-                byte[] data = new byte[4096];
-                int temp = 0;
-
-                is = conn.getInputStream();
-
-                while ((temp = is.read(data)) != -1) {
-                    os.write(data, 0, temp);
-                }
-            }
-        } catch (IOException e) {
-            log.error("Http action error!", e);
-            if (debug) {
-                e.printStackTrace();
-            }
-            // 操作失败
-            return null;
-        } finally {
-            try {
-                if (os != null) {
-                    os.close();
-                    os = null;
-                }
-                if (is != null) {
-                    is.close();
-                    is = null;
-                }
-            } catch (IOException e) {
-                log.error("Http action error!", e);
-            }
-
-            if (conn != null) {
-                conn.disconnect();
-                conn = null;
-            }
-        }
-
-        return result;
-    }
-
-    /**
-     * 获得连接请求的返回数据
-     * 
-     * @param conn
-     * 
-     * @return 字符串
-     */
-    private String getText(HttpURLConnection conn, boolean isHeadMethod) throws IOException {
-
-        StringBuilder text = new StringBuilder();
-        fileType = null;
-
-        InputStream is = null;
-        InputStreamReader sr = null;
-        BufferedReader br = null;
-
-        int code = conn.getResponseCode();
-
-        try {
-            is = code >= 400 ? conn.getErrorStream() : conn.getInputStream();
-
-            if (!isHeadMethod) {
-                sr = new InputStreamReader(is, DEFAULT_CHARSET);
-                br = new BufferedReader(sr);
-
-                char[] chars = new char[4096];
-                int length = 0;
-
-                while ((length = br.read(chars)) != -1) {
-                    text.append(chars, 0, length);
-                }
-
-            }
-            if (200 == code && conn.getHeaderField(X_UPYUN_WIDTH) != null) {
-                picWidth = conn.getHeaderField(X_UPYUN_WIDTH);
-                picHeight = conn.getHeaderField(X_UPYUN_HEIGHT);
-                picFrames = conn.getHeaderField(X_UPYUN_FRAMES);
-                picType = conn.getHeaderField(X_UPYUN_FILE_TYPE);
-            } else {
-                picWidth = picHeight = picFrames = picType = null;
-            }
-
-            if (200 == code && conn.getHeaderField(X_UPYUN_FILE_TYPE) != null) {
-                fileType = conn.getHeaderField(X_UPYUN_FILE_TYPE);
-                fileSize = conn.getHeaderField(X_UPYUN_FILE_SIZE);
-                fileDate = conn.getHeaderField(X_UPYUN_FILE_DATE);
-            } else {
-                fileType = fileSize = fileDate = null;
-            }
-        } finally {
-            if (br != null) {
-                br.close();
-                br = null;
-            }
-            if (sr != null) {
-                sr.close();
-                sr = null;
-            }
-            if (is != null) {
-                is.close();
-                is = null;
-            }
-        }
-
-        if (isHeadMethod) {
-            if (code >= 400)
-                return null;
-            return "";
-        }
-
-        if (code >= 400)
-            throw new IOException(text.toString());
-
-        return text.toString();
-    }
-
-    /**
-     * 判断字符串是否为空
-     * 
-     * @param str
-     * @return 是否为空
-     */
-    private boolean isEmpty(String str) {
-        return str == null || str.length() == 0;
-    }
-
-    /**
-     * 格式化路径参数,去除前后的空格并确保以"/"开头,最后添加"/空间名"
-     * <p>
-     * 最终构成的格式:"/空间名/文件路径"
-     * 
-     * @param path
-     *            目录路径或文件路径
-     * @return 格式化后的路径
-     */
-    private String formatPath(String path) {
-
-        if (!isEmpty(path)) {
-
-            // 去除前后的空格
-            path = path.trim();
-
-            // 确保路径以"/"开头
-            if (!path.startsWith(SEPARATOR)) {
-                return SEPARATOR + bucketName + SEPARATOR + path;
-            }
-        }
-
-        return SEPARATOR + bucketName + path;
-    }
-
-    public class FolderItem {
-
-        // 文件名
-        public String name;
-
-        // 文件类型 {file, folder}
-        public String type;
-
-        // 文件大小
-        public long size;
-
-        // 文件日期
-        public Date date;
-
-        public FolderItem(String data) {
-            String[] a = data.split("\t");
-            if (a.length == 4) {
-                this.name = a[0];
-                this.type = ("N".equals(a[1]) ? "File" : "Folder");
-                try {
-                    this.size = Long.parseLong(a[2].trim());
-                } catch (NumberFormatException e) {
-                    this.size = -1;
-                }
-                long da = 0;
-                try {
-                    da = Long.parseLong(a[3].trim());
-                } catch (NumberFormatException e) {
-                }
-                this.date = new Date(da * 1000);
-            }
-        }
-
-        @Override
-        public String toString() {
-            return "time = " + date + "  size = " + size + "  type = " + type + "  name = " + name;
-        }
-    }
-
-    /**
-     * 其他额外参数的键值和参数值
-     */
-    public enum PARAMS {
-
-        /**
-         * 缩略图类型
-         * <p>
-         * 使用场景:上传图片时若无需保存原图,而只需某种特定大小的缩略图,比如说用户头像。
-         * <p>
-         * 说明:该参数必须搭配 KEY_X_GMKERL_VALUE 使用,否则无效。另外,使用该参数后将不保存原图,切忌。
-         * <p>
-         * 可选参数:<br>
-         * 1)VALUE_FIX_MAX("fix_max"):"限定最长边,短边自适应"<br>
-         * 2)VALUE_FIX_MIN("fix_min"):"限定最短边,长边自适应"<br>
-         * 3)VALUE_FIX_WIDTH_OR_HEIGHT("fix_width_or_height"):"限定宽度和高度"<br>
-         * 4)VALUE_FIX_WIDTH("fix_width"):"限定宽度,高度自适应"<br>
-         * 5)VALUE_FIX_HEIGHT("fix_height"):"限定高度,宽度自适应"<br>
-         * 6)VALUE_FIX_BOTH("fix_both"):"固定宽度和高度"<br>
-         * 7)VALUE_FIX_SCALE("fix_scale"):"等比例缩放"<br>
-         * 8)VALUE_SQUARE("square"):"方块图,固定高固定宽"<br>
-         * 
-         * @see 参数举例:http://wiki.upyun.com/index.php?title=缩略图方式差别举例
-         */
-        KEY_X_GMKERL_TYPE("x-gmkerl-type"),
-
-        /**
-         * 缩略图参数值
-         * <p>
-         * 说明:该参数必须搭配 KEY_X_GMKERL_TYPE 使用,否则无效。具体的值需要根据 KEY_X_GMKERL_TYPE 而定。
-         */
-        KEY_X_GMKERL_VALUE("x-gmkerl-value"),
-
-        /**
-         * 缩略图质量:图片压缩质量,默认 95
-         * <p>
-         * 使用场景:用户上传高保真图片,但自身业务又无需太高质量的图片时,可以设置该参数减少文件保存的大小,从而减少空间的使用量。
-         * <p>
-         * 说明:使用该参数后将不保存原图,切忌。
-         */
-        KEY_X_GMKERL_QUALITY("x-gmkerl-quality"),
-
-        /**
-         * 图片锐化:默认锐化(true)
-         * <p>
-         * 使用场景:图片处理后质量太差,可以使用该参数模糊边缘,提高图片的清晰度或者焦距程度,使图片特定区域的色彩更加鲜明。
-         * <p>
-         * 说明:锐化不是万能的,很容易使图片不真实;另外,也无法通过锐化达到原图的效果。
-         */
-        KEY_X_GMKERL_UNSHARP("x-gmkerl-unsharp"),
-
-        /**
-         * 缩略图版本
-         * <p>
-         * 使用场景:快速处理原图,生成自定义的缩略图。
-         * <p>
-         * 说明:使用该参数前需要创建好缩略图版本号;另外,使用该参数后将不保存原图,切忌。
-         * 
-         * @see http://wiki.upyun.com/index.php?title=如何创建自定义缩略图
-         */
-        KEY_X_GMKERL_THUMBNAIL("x-gmkerl-thumbnail"),
-
-        /**
-         * 图片旋转
-         * <p>
-         * 使用场景:待上传的图片若是倾斜的,使用该参数可以直接进行强制的或自动的扶正。
-         * <p>
-         * 说明:只接受"auto","90","180","270"四种参数,其中"auto"参数根据图片 EXIF
-         * 中的信息进行自动扶正,若图片没有 EXIF 信息,则该参数无效。另外,使用该参数后将不保存原图,切忌。
-         * 
-         * @see http://wiki.upyun.com/index.php?title=图片旋转
-         */
-        KEY_X_GMKERL_ROTATE("x-gmkerl-rotate"),
-
-        /**
-         * 图片裁剪
-         * <p>
-         * 使用场景:只需要保存待上传图片的某一个部分,比如用户上传头像图片进行裁剪。
-         * <p>
-         * 说明:参数格式为x,y,width,height,且需要满足 x >= 0 && y >=0 && width > 0 && height
-         * > 0
-         * 
-         * @see http://wiki.upyun.com/index.php?title=图片裁剪
-         */
-        KEY_X_GMKERL_CROP("x-gmkerl-crop"),
-
-        /**
-         * 是否保留exif信息
-         * <p>
-         * 使用场景:对于原图包含EXIF信息,在上传图片时又进行了“破坏性处理”(比如裁剪、缩略、自定义版本等),
-         * upyun默认会删除原图的EXIF信息。 此时搭配该参数可以保留原图的EXIF信息。比如旅游应用从缩略图中获取具体的地理信息。
-         * <p>
-         * 说明:仅搭配"破坏性处理"的参数使用时有效,其他处理均无效;另外key对应的值仅设置为"true"时有效;
-         */
-        KEY_X_GMKERL_EXIF_SWITCH("x-gmkerl-exif-switch"),
-
-        /**
-         * 创建目录
-         * <p>
-         * 说明:SDK内部使用
-         */
-        KEY_MAKE_DIR("folder"),
-
-        /**
-         * 缩略图类型之 "限定最长边,短边自适应",参数为像素值,如: 150
-         */
-        VALUE_FIX_MAX("fix_max"),
-        /**
-         * 缩略图类型之 "限定最短边,长边自适应",参数为像素值,如: 150
-         */
-        VALUE_FIX_MIN("fix_min"),
-        /**
-         * 缩略图类型之 "限定宽度和高度",参数为像素值,如: 150x130
-         */
-        VALUE_FIX_WIDTH_OR_HEIGHT("fix_width_or_height"),
-        /**
-         * 缩略图类型之 "限定宽度,高度自适应",参数为像素值,如: 150
-         */
-        VALUE_FIX_WIDTH("fix_width"),
-        /**
-         * 缩略图类型之 "限定高度,宽度自适应",参数为像素值,如: 150
-         */
-        VALUE_FIX_HEIGHT("fix_height"),
-        /**
-         * 缩略图类型之 "方块图,固定高固定宽",参数为像素值,如: 150
-         */
-        VALUE_SQUARE("square"),
-        /**
-         * 缩略图类型之 "固定宽度和高度",参数为像素值,如: 150x130
-         */
-        VALUE_FIX_BOTH("fix_both"),
-        /**
-         * 缩略图类型之 "等比例缩放",参数为比例值(1-99),如: 50
-         */
-        VALUE_FIX_SCALE("fix_scale"),
-
-        /**
-         * 图片旋转之 "自动扶正"
-         */
-        VALUE_ROTATE_AUTO("auto"),
-        /**
-         * 图片旋转之 "旋转90度"
-         */
-        VALUE_ROTATE_90("90"),
-        /**
-         * 图片旋转之 "旋转180度"
-         */
-        VALUE_ROTATE_180("180"),
-        /**
-         * 图片旋转之 "旋转270度"
-         */
-        VALUE_ROTATE_270("270");
-
-        private final String value;
-
-        private PARAMS(String val) {
-            value = val;
-        }
-
-        public String getValue() {
-            return value;
-        }
-    }
-
-    public String getBucketName() {
-        return bucketName;
-    }
-
-    public String getUserName() {
-        return userName;
-    }
-
-    public String getPassword() {
-        return password;
-    }
-}
+package cn.com.qmth.stmms.common.upyun;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TimeZone;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class UpYun {
+
+    private static Logger log = LoggerFactory.getLogger(UpYun.class);
+
+    /** 默认的编码格式 */
+    public static final String DEFAULT_CHARSET = "UTF-8";
+
+    /** SKD版本号 */
+    private final String VERSION = "2.0";
+
+    /** 路径的分割符 */
+    private final String SEPARATOR = "/";
+
+    private final String AUTHORIZATION = "Authorization";
+
+    private final String DATE = "Date";
+
+    private final String CONTENT_LENGTH = "Content-Length";
+
+    public static final String CONTENT_MD5 = "Content-MD5";
+
+    private final String CONTENT_SECRET = "Content-Secret";
+
+    private final String MKDIR = "mkdir";
+
+    private final String X_UPYUN_WIDTH = "x-upyun-width";
+
+    private final String X_UPYUN_HEIGHT = "x-upyun-height";
+
+    private final String X_UPYUN_FRAMES = "x-upyun-frames";
+
+    public static final String X_UPYUN_FILE_TYPE = "x-upyun-file-type";
+
+    public static final String X_UPYUN_FILE_SIZE = "x-upyun-file-size";
+
+    public static final String X_UPYUN_FILE_DATE = "x-upyun-file-date";
+
+    private final String METHOD_HEAD = "HEAD";
+
+    private final String METHOD_GET = "GET";
+
+    private final String METHOD_PUT = "PUT";
+
+    private final String METHOD_DELETE = "DELETE";
+
+    /**
+     * 根据网络条件自动选择接入点:v0.api.upyun.com
+     */
+    public static final String ED_AUTO = "v0.api.upyun.com";
+
+    /**
+     * 电信接入点:v1.api.upyun.com
+     */
+    public static final String ED_TELECOM = "v1.api.upyun.com";
+
+    /**
+     * 联通网通接入点:v2.api.upyun.com
+     */
+    public static final String ED_CNC = "v2.api.upyun.com";
+
+    /**
+     * 移动铁通接入点:v3.api.upyun.com
+     */
+    public static final String ED_CTT = "v3.api.upyun.com";
+
+    // 默认不开启debug模式
+    public boolean debug = false;
+
+    // 默认的超时时间:30秒
+    private int timeout = 30 * 1000;
+
+    // 默认为自动识别接入点
+    private String apiDomain = ED_AUTO;
+
+    // 待上传文件的 Content-MD5 值
+    private String contentMD5 = null;
+
+    // 待上传文件的"访问密钥"
+    private String fileSecret = null;
+
+    // 空间名
+    protected String bucketName = null;
+
+    // 操作员名
+    protected String userName = null;
+
+    // 操作员密码
+    protected String password = null;
+
+    // 图片信息的参数
+    protected String picWidth = null;
+
+    protected String picHeight = null;
+
+    protected String picFrames = null;
+
+    protected String picType = null;
+
+    // 文件信息的参数
+    protected String fileType = null;
+
+    protected String fileSize = null;
+
+    protected String fileDate = null;
+
+    /**
+     * 初始化 UpYun 存储接口
+     * 
+     * @param bucketName
+     *            空间名称
+     * @param userName
+     *            操作员名称
+     * @param password
+     *            密码,不需要MD5加密
+     * @return UpYun object
+     */
+    public UpYun(String bucketName, String userName, String password) {
+        this.bucketName = bucketName;
+        this.userName = userName;
+        this.password = md5(password);
+    }
+
+    /**
+     * 切换 API 接口的域名接入点
+     * <p>
+     * 可选参数:<br>
+     * 1) UpYun.ED_AUTO(v0.api.upyun.com):默认,根据网络条件自动选择接入点 <br>
+     * 2) UpYun.ED_TELECOM(v1.api.upyun.com):电信接入点<br>
+     * 3) UpYun.ED_CNC(v2.api.upyun.com):联通网通接入点<br>
+     * 4) UpYun.ED_CTT(v3.api.upyun.com):移动铁通接入点
+     * 
+     * @param domain
+     *            域名接入点
+     */
+    public void setApiDomain(String domain) {
+        this.apiDomain = domain;
+    }
+
+    /**
+     * 查看当前的域名接入点
+     * 
+     * @return
+     */
+    public String getApiDomain() {
+        return apiDomain;
+    }
+
+    /**
+     * 设置连接超时时间,默认为30秒
+     * 
+     * @param second
+     *            秒数,60即为一分钟超时
+     */
+    public void setTimeout(int second) {
+        this.timeout = second * 1000;
+    }
+
+    /**
+     * 查看当前的超时时间
+     * 
+     * @return
+     */
+    public int getTimeout() {
+        return timeout;
+    }
+
+    /**
+     * 查看当前是否是debug模式
+     * 
+     * @return
+     */
+    public boolean isDebug() {
+        return debug;
+    }
+
+    /**
+     * 设置是否开启debug模式
+     * 
+     * @param debug
+     */
+    public void setDebug(boolean debug) {
+        this.debug = debug;
+    }
+
+    /**
+     * 设置待上传文件的 Content-MD5 值
+     * <p>
+     * 如果又拍云服务端收到的文件MD5值与用户设置的不一致,将回报 406 Not Acceptable 错误
+     * 
+     * @param md5Value
+     *            文件 MD5 校验后的内容
+     */
+    public void setContentMD5(String md5Value) {
+        this.contentMD5 = md5Value;
+    }
+
+    /**
+     * 设置待上传文件的"访问密钥"
+     * <p>
+     * 注意:<br>
+     * 仅支持图片空!设置密钥后,无法根据原文件URL直接访问,需带 URL 后面加上 (缩略图间隔标志符+密钥) 进行访问
+     * <p>
+     * 举例:<br>
+     * 如果缩略图间隔标志符为"!",密钥为"bac",上传文件路径为"/folder/test.jpg",<br>
+     * 那么该图片的对外访问地址为:http://空间域名 /folder/test.jpg!bac
+     * 
+     * @param secret
+     *            密钥字符串
+     */
+    public void setFileSecret(String secret) {
+        this.fileSecret = secret;
+    }
+
+    public String getPicWidth() {
+        return picWidth;
+    }
+
+    public String getPicHeight() {
+        return picHeight;
+    }
+
+    public String getPicFrames() {
+        return picFrames;
+    }
+
+    public String getPicType() {
+        return picType;
+    }
+
+    /**
+     * 获取当前SDK的版本号
+     * 
+     * @return SDK版本号
+     */
+    public String version() {
+        return VERSION;
+    }
+
+    /**
+     * 获取总体空间的占用量
+     * 
+     * @param path
+     *            目标路径
+     * @return 空间占用量,失败时返回 -1
+     */
+    public long getBucketUsage() {
+        return getFolderUsage("/");
+    }
+
+    /**
+     * 获取某个子目录的占用量
+     * 
+     * @param path
+     *            目标路径
+     * @return 空间占用量,失败时返回 -1
+     */
+    public long getFolderUsage(String path) {
+
+        long usage = -1;
+
+        String result = HttpAction(METHOD_GET, formatPath(path) + "/?usage");
+
+        if (!isEmpty(result)) {
+
+            try {
+                usage = Long.parseLong(result.trim());
+            } catch (NumberFormatException e) {
+            }
+        }
+
+        return usage;
+    }
+
+    /**
+     * 上传文件
+     * 
+     * @param filePath
+     *            文件路径(包含文件名)
+     * @param datas
+     *            文件内容
+     * 
+     * @return true or false
+     */
+    public boolean writeFile(String filePath, byte[] datas) {
+        return writeFile(filePath, datas, false, null);
+    }
+
+    /**
+     * 上传文件
+     * 
+     * @param filePath
+     *            文件路径(包含文件名)
+     * @param datas
+     *            文件内容
+     * @param auto
+     *            是否自动创建父级目录(最多10级)
+     * 
+     * @return true or false
+     */
+    public boolean writeFile(String filePath, byte[] datas, boolean auto) {
+        return writeFile(filePath, datas, auto, null);
+    }
+
+    /**
+     * 上传文件
+     * 
+     * @param filePath
+     *            文件路径(包含文件名)
+     * @param datas
+     *            文件内容
+     * @param auto
+     *            是否自动创建父级目录(最多10级)
+     * @param params
+     *            额外参数
+     * 
+     * @return true or false
+     */
+    public boolean writeFile(String filePath, byte[] datas, boolean auto, Map<String, String> params) {
+
+        return HttpAction(METHOD_PUT, formatPath(filePath), datas, null, auto, params) != null;
+    }
+
+    /**
+     * 上传文件
+     * 
+     * @param filePath
+     *            文件路径(包含文件名)
+     * @param String
+     *            datas 文件内容
+     * 
+     * @return true or false
+     */
+    public boolean writeFile(String filePath, String datas) {
+        return writeFile(filePath, datas, false, null);
+    }
+
+    /**
+     * 上传文件
+     * 
+     * @param filePath
+     *            文件路径(包含文件名)
+     * @param String
+     *            datas 文件内容
+     * @param auto
+     *            是否自动创建父级目录(最多10级)
+     * 
+     * @return true or false
+     */
+    public boolean writeFile(String filePath, String datas, boolean auto) {
+        return writeFile(filePath, datas, auto, null);
+    }
+
+    /**
+     * 上传文件
+     * 
+     * @param filePath
+     *            文件路径(包含文件名)
+     * @param String
+     *            datas 文件内容
+     * @param auto
+     *            是否自动创建父级目录(最多10级)
+     * @param params
+     *            额外参数
+     * 
+     * @return true or false
+     */
+    public boolean writeFile(String filePath, String datas, boolean auto, Map<String, String> params) {
+
+        boolean result = false;
+
+        try {
+            result = writeFile(filePath, datas.getBytes(DEFAULT_CHARSET), auto, params);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        return result;
+    }
+
+    /**
+     * 上传文件
+     * 
+     * @param filePath
+     *            文件路径(包含文件名)
+     * @param file
+     *            待上传的文件
+     * 
+     * @return true or false
+     * @throws IOException
+     */
+    public boolean writeFile(String filePath, File file) throws IOException {
+        return writeFile(filePath, file, false, null);
+    }
+
+    /**
+     * 上传文件
+     * 
+     * @param filePath
+     *            文件路径(包含文件名)
+     * @param file
+     *            待上传的文件
+     * @param auto
+     *            是否自动创建父级目录(最多10级)
+     * 
+     * @return true or false
+     * @throws IOException
+     */
+    public boolean writeFile(String filePath, File file, boolean auto) throws IOException {
+        return writeFile(filePath, file, auto, null);
+    }
+
+    /**
+     * 上传文件
+     * 
+     * @param filePath
+     *            文件路径(包含文件名)
+     * @param file
+     *            待上传的文件
+     * @param auto
+     *            是否自动创建父级目录(最多10级)
+     * @param params
+     *            额外参数
+     * 
+     * @return true or false
+     * @throws IOException
+     */
+    public boolean writeFile(String filePath, File file, boolean auto, Map<String, String> params) throws IOException {
+
+        filePath = formatPath(filePath);
+
+        InputStream is = null;
+        OutputStream os = null;
+        HttpURLConnection conn = null;
+
+        try {
+            // 读取待上传的文件
+            is = new FileInputStream(file);
+
+            // 获取链接
+            URL url = new URL("http://" + apiDomain + filePath);
+            conn = (HttpURLConnection) url.openConnection();
+
+            // 设置必要参数
+            conn.setConnectTimeout(timeout);
+            conn.setRequestMethod(METHOD_PUT);
+            conn.setUseCaches(false);
+            conn.setDoOutput(true);
+
+            // 设置时间
+            conn.setRequestProperty(DATE, getGMTDate());
+            // 设置签名
+            conn.setRequestProperty(AUTHORIZATION, sign(conn, filePath, is.available()));
+
+            // 设置文件的 MD5 参数
+            if (!isEmpty(this.contentMD5)) {
+                conn.setRequestProperty(CONTENT_MD5, this.contentMD5);
+                this.contentMD5 = null;
+            }
+
+            // 设置文件的访问密钥
+            if (!isEmpty(this.fileSecret)) {
+                conn.setRequestProperty(CONTENT_SECRET, this.fileSecret);
+                this.fileSecret = null;
+            }
+
+            // 是否自动创建父级目录
+            if (auto) {
+                conn.setRequestProperty(MKDIR, "true");
+            }
+
+            // 设置额外的参数,如图片缩略图等
+            if (params != null && !params.isEmpty()) {
+
+                for (Map.Entry<String, String> param : params.entrySet()) {
+                    conn.setRequestProperty(param.getKey(), param.getValue());
+                }
+            }
+
+            // 创建链接
+            conn.connect();
+
+            os = conn.getOutputStream();
+            byte[] data = new byte[4096];
+            int temp = 0;
+
+            // 上传文件内容
+            while ((temp = is.read(data)) != -1) {
+                os.write(data, 0, temp);
+            }
+
+            // 获取返回的信息
+            getText(conn, false);
+
+            // 上传成功
+            return true;
+
+        } catch (IOException e) {
+            if (debug)
+                e.printStackTrace();
+
+            // 上传失败
+            return false;
+
+        } finally {
+
+            if (os != null) {
+                os.close();
+                os = null;
+            }
+            if (is != null) {
+                is.close();
+                is = null;
+            }
+            if (conn != null) {
+                conn.disconnect();
+                conn = null;
+            }
+        }
+    }
+
+    /**
+     * 读取文件
+     * 
+     * @param filePath
+     *            文件路径(包含文件名)
+     * 
+     * @return 文件内容 或 null
+     */
+    public String readFile(String filePath) {
+        return HttpAction(METHOD_GET, formatPath(filePath));
+    }
+
+    /**
+     * 读取文件
+     * 
+     * @param filePath
+     *            文件路径(包含文件名)
+     * @param out
+     *            输出流
+     * 
+     * @return true or false
+     */
+    public boolean readFile(String filePath, OutputStream out) {
+
+        String result = HttpAction(METHOD_GET, formatPath(filePath), null, out, false);
+
+        return "".equals(result);
+    }
+
+    /**
+     * 获取文件信息
+     * 
+     * @param filePath
+     *            文件路径(包含文件名)
+     * 
+     * @return 文件信息 或 null
+     */
+    public Map<String, String> getFileInfo(String filePath) {
+
+        HttpAction(METHOD_HEAD, formatPath(filePath));
+
+        // 判断是否存在文件信息
+        if (isEmpty(fileType) && isEmpty(fileSize) && isEmpty(fileDate)) {
+            return null;
+        }
+
+        Map<String, String> mp = new HashMap<String, String>();
+        mp.put("type", fileType);
+        mp.put("size", fileSize);
+        mp.put("date", fileDate);
+
+        return mp;
+    }
+
+    /**
+     * 删除文件
+     * 
+     * @param filePath
+     *            文件路径(包含文件名)
+     * 
+     * @return true or false
+     */
+    public boolean deleteFile(String filePath) {
+
+        return HttpAction(METHOD_DELETE, formatPath(filePath)) != null;
+    }
+
+    /**
+     * 创建目录
+     * 
+     * @param path
+     *            目录路径
+     * 
+     * @return true or false
+     */
+    public boolean mkDir(String path) {
+        return mkDir(path, false);
+    }
+
+    /**
+     * 创建目录
+     * 
+     * @param path
+     *            目录路径
+     * @param auto
+     *            是否自动创建父级目录(最多10级)
+     * 
+     * @return true or false
+     */
+    public boolean mkDir(String path, boolean auto) {
+
+        Map<String, String> params = new HashMap<String, String>(1);
+        params.put(PARAMS.KEY_MAKE_DIR.getValue(), "true");
+
+        String result = HttpAction(METHOD_PUT, formatPath(path), null, null, auto, params);
+
+        return result != null;
+    }
+
+    /**
+     * 读取目录列表
+     * 
+     * @param path
+     *            目录路径
+     * 
+     * @return List<FolderItem> 或 null
+     */
+    public List<FolderItem> readDir(String path) {
+
+        String result = HttpAction(METHOD_GET, formatPath(path) + SEPARATOR);
+
+        if (isEmpty(result))
+            return null;
+
+        List<FolderItem> list = new LinkedList<FolderItem>();
+
+        String[] datas = result.split("\n");
+
+        for (int i = 0; i < datas.length; i++) {
+            if (datas[i].indexOf("\t") > 0) {
+                list.add(new FolderItem(datas[i]));
+            }
+        }
+        return list;
+    }
+
+    /**
+     * 删除目录
+     * 
+     * @param path
+     *            目录路径
+     * 
+     * @return true or false
+     */
+    public boolean rmDir(String path) {
+        return HttpAction(METHOD_DELETE, formatPath(path)) != null;
+    }
+
+    /**
+     * 获取上传文件后的信息(仅图片空间有返回数据)
+     * 
+     * @param key
+     *            信息字段名(x-upyun-width、x-upyun-height、x-upyun-frames、x-upyun-file
+     *            -type)
+     * 
+     * @return value or NULL
+     * @deprecated
+     */
+    public String getWritedFileInfo(String key) {
+
+        if (isEmpty(picWidth))
+            return null;
+
+        if (X_UPYUN_WIDTH.equals(key))
+            return picWidth;
+        if (X_UPYUN_HEIGHT.equals(key))
+            return picHeight;
+        if (X_UPYUN_FRAMES.equals(key))
+            return picFrames;
+        if (X_UPYUN_FILE_TYPE.equals(key))
+            return picType;
+
+        return null;
+    }
+
+    /**
+     * 对字符串进行 MD5 加密
+     * 
+     * @param str
+     *            待加密字符串
+     * 
+     * @return 加密后字符串
+     */
+    public static String md5(String str) {
+        char hexDigits[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
+        MessageDigest md5 = null;
+        try {
+            md5 = MessageDigest.getInstance("MD5");
+            md5.update(str.getBytes(DEFAULT_CHARSET));
+        } catch (NoSuchAlgorithmException e) {
+            e.printStackTrace();
+            throw new RuntimeException(e.getMessage());
+        } catch (UnsupportedEncodingException e) {
+            e.printStackTrace();
+            throw new RuntimeException(e.getMessage());
+        }
+        byte[] encodedValue = md5.digest();
+        int j = encodedValue.length;
+        char finalValue[] = new char[j * 2];
+        int k = 0;
+        for (int i = 0; i < j; i++) {
+            byte encoded = encodedValue[i];
+            finalValue[k++] = hexDigits[encoded >> 4 & 0xf];
+            finalValue[k++] = hexDigits[encoded & 0xf];
+        }
+
+        return new String(finalValue);
+    }
+
+    /**
+     * 对文件进行 MD5 加密
+     * 
+     * @param file
+     *            待加密的文件
+     * 
+     * @return 文件加密后的 MD5 值
+     * @throws IOException
+     */
+    public static String md5(File file) throws IOException {
+        FileInputStream is = new FileInputStream(file);
+        char hexDigits[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
+        MessageDigest md5 = null;
+        try {
+            md5 = MessageDigest.getInstance("MD5");
+            int n = 0;
+            byte[] buffer = new byte[1024];
+            do {
+                n = is.read(buffer);
+                if (n > 0) {
+                    md5.update(buffer, 0, n);
+                }
+            } while (n != -1);
+            is.skip(0);
+        } catch (NoSuchAlgorithmException e) {
+            e.printStackTrace();
+            throw new RuntimeException(e.getMessage());
+        } finally {
+            is.close();
+        }
+
+        byte[] encodedValue = md5.digest();
+
+        int j = encodedValue.length;
+        char finalValue[] = new char[j * 2];
+        int k = 0;
+        for (int i = 0; i < j; i++) {
+            byte encoded = encodedValue[i];
+            finalValue[k++] = hexDigits[encoded >> 4 & 0xf];
+            finalValue[k++] = hexDigits[encoded & 0xf];
+        }
+
+        return new String(finalValue);
+    }
+
+    /**
+     * 获取 GMT 格式时间戳
+     * 
+     * @return GMT 格式时间戳
+     */
+    private String getGMTDate() {
+        SimpleDateFormat formater = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
+        formater.setTimeZone(TimeZone.getTimeZone("GMT"));
+        return formater.format(new Date());
+    }
+
+    /**
+     * 计算签名
+     * 
+     * @param conn
+     *            连接
+     * @param uri
+     *            请求地址
+     * @param length
+     *            请求所发Body数据长度
+     * 
+     * @return 签名字符串
+     */
+    private String sign(HttpURLConnection conn, String uri, long length) {
+        String sign = conn.getRequestMethod() + "&" + uri + "&" + conn.getRequestProperty(DATE) + "&" + length + "&"
+                + password;
+        return "UpYun " + userName + ":" + md5(sign);
+    }
+
+    /**
+     * 连接处理逻辑
+     * 
+     * @param method
+     *            请求方式 {GET, POST, PUT, DELETE}
+     * @param uri
+     *            请求地址
+     * 
+     * @return 请求结果(字符串)或 null
+     */
+    private String HttpAction(String method, String uri) {
+        return HttpAction(method, uri, null, null, false);
+    }
+
+    /**
+     * 连接处理逻辑
+     * 
+     * @param method
+     *            请求方式 {GET, POST, PUT, DELETE}
+     * @param uri
+     *            请求地址
+     * @param datas
+     *            该请求所需发送数据(可为 null)
+     * @param outFile
+     *            文件描述符(可为 null)
+     * @param auto
+     *            自动创建父级目录(最多10级)
+     * 
+     * @return 请求结果(字符串)或 null
+     */
+    private String HttpAction(String method, String uri, byte[] datas, OutputStream out, boolean auto) {
+
+        return HttpAction(method, uri, datas, out, auto, null);
+    }
+
+    /**
+     * 连接处理逻辑
+     * 
+     * @param method
+     *            请求方式 {GET, POST, PUT, DELETE}
+     * @param uri
+     *            请求地址
+     * @param datas
+     *            该请求所需发送数据(可为 null)
+     * @param outFile
+     *            文件描述符(可为 null)
+     * @param auto
+     *            自动创建父级目录(最多10级)
+     * @param params
+     *            额外参数
+     * 
+     * @return 请求结果(字符串)或 null
+     */
+    @SuppressWarnings("resource")
+    private String HttpAction(String method, String uri, byte[] datas, OutputStream out, boolean auto,
+            Map<String, String> params) {
+        String result = null;
+
+        HttpURLConnection conn = null;
+        OutputStream os = null;
+        InputStream is = null;
+
+        try {
+            // 获取链接
+            String prefix = apiDomain.contains("upyun") ? "https://" : "http://";
+            URL url = new URL(prefix + apiDomain + uri);
+            conn = (HttpURLConnection) url.openConnection();
+
+            // 设置必要参数
+            conn.setConnectTimeout(timeout);
+            conn.setRequestMethod(method);
+            conn.setUseCaches(false);
+            conn.setDoOutput(true);
+
+            // 设置时间
+            conn.setRequestProperty(DATE, getGMTDate());
+
+            // 是否自动创建父级目录
+            if (auto) {
+                conn.setRequestProperty(MKDIR, "true");
+            }
+
+            long contentLength = 0;
+            if (datas == null)
+                conn.setRequestProperty(CONTENT_LENGTH, "0");
+            else {
+                contentLength = datas.length;
+                conn.setRequestProperty(CONTENT_LENGTH, String.valueOf(datas.length));
+
+                // 设置文件的 MD5 参数
+                if (!isEmpty(this.contentMD5)) {
+                    conn.setRequestProperty(CONTENT_MD5, this.contentMD5);
+                    this.contentMD5 = null;
+                }
+                // 设置文件的访问密钥
+                if (!isEmpty(this.fileSecret)) {
+                    conn.setRequestProperty(CONTENT_SECRET, this.fileSecret);
+                    this.fileSecret = null;
+                }
+            }
+
+            // 设置签名
+            conn.setRequestProperty(AUTHORIZATION, sign(conn, uri, contentLength));
+
+            // 是否是创建文件目录
+            boolean isFolder = false;
+
+            // 设置额外的参数,如图片缩略图等
+            if (params != null && !params.isEmpty()) {
+
+                isFolder = !isEmpty(params.get(PARAMS.KEY_MAKE_DIR.getValue()));
+
+                for (Map.Entry<String, String> param : params.entrySet()) {
+                    conn.setRequestProperty(param.getKey(), param.getValue());
+                }
+            }
+
+            // 创建链接
+            conn.connect();
+
+            if (datas != null) {
+                os = conn.getOutputStream();
+                os.write(datas);
+                os.flush();
+            }
+
+            if (isFolder) {
+                os = conn.getOutputStream();
+                os.flush();
+            }
+
+            if (out == null) {
+
+                result = getText(conn, METHOD_HEAD.equals(method));
+
+            } else {
+                result = "";
+
+                byte[] data = new byte[4096];
+                int temp = 0;
+
+                is = conn.getInputStream();
+
+                while ((temp = is.read(data)) != -1) {
+                    out.write(data, 0, temp);
+                }
+            }
+        } catch (IOException e) {
+            log.error("Http action error!", e);
+            if (debug) {
+                e.printStackTrace();
+            }
+            // 操作失败
+            return null;
+        } finally {
+            try {
+                if (os != null) {
+                    os.close();
+                    os = null;
+                }
+                if (is != null) {
+                    is.close();
+                    is = null;
+                }
+            } catch (IOException e) {
+                log.error("Http action error!", e);
+            }
+
+            if (conn != null) {
+                conn.disconnect();
+                conn = null;
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * 获得连接请求的返回数据
+     * 
+     * @param conn
+     * 
+     * @return 字符串
+     */
+    private String getText(HttpURLConnection conn, boolean isHeadMethod) throws IOException {
+
+        StringBuilder text = new StringBuilder();
+        fileType = null;
+
+        InputStream is = null;
+        InputStreamReader sr = null;
+        BufferedReader br = null;
+
+        int code = conn.getResponseCode();
+
+        try {
+            is = code >= 400 ? conn.getErrorStream() : conn.getInputStream();
+
+            if (!isHeadMethod) {
+                sr = new InputStreamReader(is, DEFAULT_CHARSET);
+                br = new BufferedReader(sr);
+
+                char[] chars = new char[4096];
+                int length = 0;
+
+                while ((length = br.read(chars)) != -1) {
+                    text.append(chars, 0, length);
+                }
+
+            }
+            if (200 == code && conn.getHeaderField(X_UPYUN_WIDTH) != null) {
+                picWidth = conn.getHeaderField(X_UPYUN_WIDTH);
+                picHeight = conn.getHeaderField(X_UPYUN_HEIGHT);
+                picFrames = conn.getHeaderField(X_UPYUN_FRAMES);
+                picType = conn.getHeaderField(X_UPYUN_FILE_TYPE);
+            } else {
+                picWidth = picHeight = picFrames = picType = null;
+            }
+
+            if (200 == code && conn.getHeaderField(X_UPYUN_FILE_TYPE) != null) {
+                fileType = conn.getHeaderField(X_UPYUN_FILE_TYPE);
+                fileSize = conn.getHeaderField(X_UPYUN_FILE_SIZE);
+                fileDate = conn.getHeaderField(X_UPYUN_FILE_DATE);
+            } else {
+                fileType = fileSize = fileDate = null;
+            }
+        } finally {
+            if (br != null) {
+                br.close();
+                br = null;
+            }
+            if (sr != null) {
+                sr.close();
+                sr = null;
+            }
+            if (is != null) {
+                is.close();
+                is = null;
+            }
+        }
+
+        if (isHeadMethod) {
+            if (code >= 400)
+                return null;
+            return "";
+        }
+
+        if (code >= 400)
+            throw new IOException(text.toString());
+
+        return text.toString();
+    }
+
+    /**
+     * 判断字符串是否为空
+     * 
+     * @param str
+     * @return 是否为空
+     */
+    private boolean isEmpty(String str) {
+        return str == null || str.length() == 0;
+    }
+
+    /**
+     * 格式化路径参数,去除前后的空格并确保以"/"开头,最后添加"/空间名"
+     * <p>
+     * 最终构成的格式:"/空间名/文件路径"
+     * 
+     * @param path
+     *            目录路径或文件路径
+     * @return 格式化后的路径
+     */
+    private String formatPath(String path) {
+
+        if (!isEmpty(path)) {
+
+            // 去除前后的空格
+            path = path.trim();
+
+            // 确保路径以"/"开头
+            if (!path.startsWith(SEPARATOR)) {
+                return SEPARATOR + bucketName + SEPARATOR + path;
+            }
+        }
+
+        return SEPARATOR + bucketName + path;
+    }
+
+    public class FolderItem {
+
+        // 文件名
+        public String name;
+
+        // 文件类型 {file, folder}
+        public String type;
+
+        // 文件大小
+        public long size;
+
+        // 文件日期
+        public Date date;
+
+        public FolderItem(String data) {
+            String[] a = data.split("\t");
+            if (a.length == 4) {
+                this.name = a[0];
+                this.type = ("N".equals(a[1]) ? "File" : "Folder");
+                try {
+                    this.size = Long.parseLong(a[2].trim());
+                } catch (NumberFormatException e) {
+                    this.size = -1;
+                }
+                long da = 0;
+                try {
+                    da = Long.parseLong(a[3].trim());
+                } catch (NumberFormatException e) {
+                }
+                this.date = new Date(da * 1000);
+            }
+        }
+
+        @Override
+        public String toString() {
+            return "time = " + date + "  size = " + size + "  type = " + type + "  name = " + name;
+        }
+    }
+
+    /**
+     * 其他额外参数的键值和参数值
+     */
+    public enum PARAMS {
+
+        /**
+         * 缩略图类型
+         * <p>
+         * 使用场景:上传图片时若无需保存原图,而只需某种特定大小的缩略图,比如说用户头像。
+         * <p>
+         * 说明:该参数必须搭配 KEY_X_GMKERL_VALUE 使用,否则无效。另外,使用该参数后将不保存原图,切忌。
+         * <p>
+         * 可选参数:<br>
+         * 1)VALUE_FIX_MAX("fix_max"):"限定最长边,短边自适应"<br>
+         * 2)VALUE_FIX_MIN("fix_min"):"限定最短边,长边自适应"<br>
+         * 3)VALUE_FIX_WIDTH_OR_HEIGHT("fix_width_or_height"):"限定宽度和高度"<br>
+         * 4)VALUE_FIX_WIDTH("fix_width"):"限定宽度,高度自适应"<br>
+         * 5)VALUE_FIX_HEIGHT("fix_height"):"限定高度,宽度自适应"<br>
+         * 6)VALUE_FIX_BOTH("fix_both"):"固定宽度和高度"<br>
+         * 7)VALUE_FIX_SCALE("fix_scale"):"等比例缩放"<br>
+         * 8)VALUE_SQUARE("square"):"方块图,固定高固定宽"<br>
+         * 
+         * @see 参数举例:http://wiki.upyun.com/index.php?title=缩略图方式差别举例
+         */
+        KEY_X_GMKERL_TYPE("x-gmkerl-type"),
+
+        /**
+         * 缩略图参数值
+         * <p>
+         * 说明:该参数必须搭配 KEY_X_GMKERL_TYPE 使用,否则无效。具体的值需要根据 KEY_X_GMKERL_TYPE 而定。
+         */
+        KEY_X_GMKERL_VALUE("x-gmkerl-value"),
+
+        /**
+         * 缩略图质量:图片压缩质量,默认 95
+         * <p>
+         * 使用场景:用户上传高保真图片,但自身业务又无需太高质量的图片时,可以设置该参数减少文件保存的大小,从而减少空间的使用量。
+         * <p>
+         * 说明:使用该参数后将不保存原图,切忌。
+         */
+        KEY_X_GMKERL_QUALITY("x-gmkerl-quality"),
+
+        /**
+         * 图片锐化:默认锐化(true)
+         * <p>
+         * 使用场景:图片处理后质量太差,可以使用该参数模糊边缘,提高图片的清晰度或者焦距程度,使图片特定区域的色彩更加鲜明。
+         * <p>
+         * 说明:锐化不是万能的,很容易使图片不真实;另外,也无法通过锐化达到原图的效果。
+         */
+        KEY_X_GMKERL_UNSHARP("x-gmkerl-unsharp"),
+
+        /**
+         * 缩略图版本
+         * <p>
+         * 使用场景:快速处理原图,生成自定义的缩略图。
+         * <p>
+         * 说明:使用该参数前需要创建好缩略图版本号;另外,使用该参数后将不保存原图,切忌。
+         * 
+         * @see http://wiki.upyun.com/index.php?title=如何创建自定义缩略图
+         */
+        KEY_X_GMKERL_THUMBNAIL("x-gmkerl-thumbnail"),
+
+        /**
+         * 图片旋转
+         * <p>
+         * 使用场景:待上传的图片若是倾斜的,使用该参数可以直接进行强制的或自动的扶正。
+         * <p>
+         * 说明:只接受"auto","90","180","270"四种参数,其中"auto"参数根据图片 EXIF
+         * 中的信息进行自动扶正,若图片没有 EXIF 信息,则该参数无效。另外,使用该参数后将不保存原图,切忌。
+         * 
+         * @see http://wiki.upyun.com/index.php?title=图片旋转
+         */
+        KEY_X_GMKERL_ROTATE("x-gmkerl-rotate"),
+
+        /**
+         * 图片裁剪
+         * <p>
+         * 使用场景:只需要保存待上传图片的某一个部分,比如用户上传头像图片进行裁剪。
+         * <p>
+         * 说明:参数格式为x,y,width,height,且需要满足 x >= 0 && y >=0 && width > 0 && height
+         * > 0
+         * 
+         * @see http://wiki.upyun.com/index.php?title=图片裁剪
+         */
+        KEY_X_GMKERL_CROP("x-gmkerl-crop"),
+
+        /**
+         * 是否保留exif信息
+         * <p>
+         * 使用场景:对于原图包含EXIF信息,在上传图片时又进行了“破坏性处理”(比如裁剪、缩略、自定义版本等),
+         * upyun默认会删除原图的EXIF信息。 此时搭配该参数可以保留原图的EXIF信息。比如旅游应用从缩略图中获取具体的地理信息。
+         * <p>
+         * 说明:仅搭配"破坏性处理"的参数使用时有效,其他处理均无效;另外key对应的值仅设置为"true"时有效;
+         */
+        KEY_X_GMKERL_EXIF_SWITCH("x-gmkerl-exif-switch"),
+
+        /**
+         * 创建目录
+         * <p>
+         * 说明:SDK内部使用
+         */
+        KEY_MAKE_DIR("folder"),
+
+        /**
+         * 缩略图类型之 "限定最长边,短边自适应",参数为像素值,如: 150
+         */
+        VALUE_FIX_MAX("fix_max"),
+        /**
+         * 缩略图类型之 "限定最短边,长边自适应",参数为像素值,如: 150
+         */
+        VALUE_FIX_MIN("fix_min"),
+        /**
+         * 缩略图类型之 "限定宽度和高度",参数为像素值,如: 150x130
+         */
+        VALUE_FIX_WIDTH_OR_HEIGHT("fix_width_or_height"),
+        /**
+         * 缩略图类型之 "限定宽度,高度自适应",参数为像素值,如: 150
+         */
+        VALUE_FIX_WIDTH("fix_width"),
+        /**
+         * 缩略图类型之 "限定高度,宽度自适应",参数为像素值,如: 150
+         */
+        VALUE_FIX_HEIGHT("fix_height"),
+        /**
+         * 缩略图类型之 "方块图,固定高固定宽",参数为像素值,如: 150
+         */
+        VALUE_SQUARE("square"),
+        /**
+         * 缩略图类型之 "固定宽度和高度",参数为像素值,如: 150x130
+         */
+        VALUE_FIX_BOTH("fix_both"),
+        /**
+         * 缩略图类型之 "等比例缩放",参数为比例值(1-99),如: 50
+         */
+        VALUE_FIX_SCALE("fix_scale"),
+
+        /**
+         * 图片旋转之 "自动扶正"
+         */
+        VALUE_ROTATE_AUTO("auto"),
+        /**
+         * 图片旋转之 "旋转90度"
+         */
+        VALUE_ROTATE_90("90"),
+        /**
+         * 图片旋转之 "旋转180度"
+         */
+        VALUE_ROTATE_180("180"),
+        /**
+         * 图片旋转之 "旋转270度"
+         */
+        VALUE_ROTATE_270("270");
+
+        private final String value;
+
+        private PARAMS(String val) {
+            value = val;
+        }
+
+        public String getValue() {
+            return value;
+        }
+    }
+
+    public String getBucketName() {
+        return bucketName;
+    }
+
+    public String getUserName() {
+        return userName;
+    }
+
+    public String getPassword() {
+        return password;
+    }
+}

+ 83 - 72
stmms-common/src/main/java/cn/com/qmth/stmms/common/utils/PictureUrlBuilder.java

@@ -1,72 +1,83 @@
-package cn.com.qmth.stmms.common.utils;
-
-import java.text.MessageFormat;
-import java.util.LinkedList;
-import java.util.List;
-
-import org.apache.commons.lang3.StringUtils;
-
-/**
- * 答题卡扫描大图URL构造工具类
- * 
- * @author LS
- * 
- */
-public class PictureUrlBuilder {
-
-    private static final String SHEET_URL_TEMPLATE = "/{0}-{1}/{2}/{3}-{4}.{5}";
-
-    private static final String SLICE_URL_TEMPLATE = "/{0}-{1}/{2}/{3}-{4}.{5}";
-
-    private static final String ANSWER_URL_TEMPLATE = "/{0}/answer/{1}.{2}";
-
-    private static final String PAPER_URL_TEMPLATE = "/{0}/paper/{1}.{2}";
-
-    private static final String PACKAGE_URL_TEMPLATE = "/{0}/{1}/{2}.{3}";
-
-    private static final String DEFAULT_SUFFIX = "jpg";
-
-    private static final String DOCUMENT_SUFFIX = "pdf";
-
-    public static List<String> getSheetUrls(int examId, int campusId, String subjectCode, String examNumber, int count) {
-        List<String> list = new LinkedList<String>();
-        if (StringUtils.isNotEmpty(subjectCode) && count > 0) {
-            for (int i = 1; i <= count; i++) {
-                list.add(MessageFormat.format(SHEET_URL_TEMPLATE, String.valueOf(examId), String.valueOf(campusId),
-                        subjectCode, examNumber, String.valueOf(i), DEFAULT_SUFFIX));
-            }
-        }
-        return list;
-    }
-
-    public static List<String> getSliceUrls(int examId, int campusId, String subjectCode, String examNumber, int start,
-            int count) {
-        List<String> list = new LinkedList<String>();
-        if (StringUtils.isNotEmpty(subjectCode) && count > 0) {
-            for (int i = start; i <= count; i++) {
-                list.add(MessageFormat.format(SLICE_URL_TEMPLATE, String.valueOf(examId), String.valueOf(campusId),
-                        subjectCode, examNumber, String.valueOf(i), DEFAULT_SUFFIX));
-            }
-        }
-        return list;
-    }
-
-    public static String getAnswerUrl(int examId, String subjectCode) {
-        return MessageFormat.format(ANSWER_URL_TEMPLATE, String.valueOf(examId), subjectCode, DOCUMENT_SUFFIX);
-    }
-
-    public static String getPaperUrl(int examId, String subjectCode) {
-        return MessageFormat.format(PAPER_URL_TEMPLATE, String.valueOf(examId), subjectCode, DOCUMENT_SUFFIX);
-    }
-
-    public static List<String> getPackageUrls(int examId, String packageCode, int count) {
-        List<String> list = new LinkedList<String>();
-        if (StringUtils.isNotEmpty(packageCode) && count > 0) {
-            for (int i = 1; i <= count; i++) {
-                list.add(MessageFormat.format(PACKAGE_URL_TEMPLATE, String.valueOf(examId), packageCode,
-                        String.valueOf(i), DEFAULT_SUFFIX));
-            }
-        }
-        return list;
-    }
-}
+package cn.com.qmth.stmms.common.utils;
+
+import java.text.MessageFormat;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * 答题卡扫描大图URL构造工具类
+ * 
+ * @author LS
+ * 
+ */
+public class PictureUrlBuilder {
+
+    private static final String SHEET_URL_TEMPLATE = "/{0}-{1}/{2}/{3}-{4}.{5}";
+
+    private static final String SLICE_URL_TEMPLATE = "/{0}-{1}/{2}/{3}-{4}.{5}";
+
+    private static final String ANSWER_URL_TEMPLATE = "/{0}/answer/{1}.{2}";
+
+    private static final String PAPER_URL_TEMPLATE = "/{0}/paper/{1}.{2}";
+
+    private static final String PACKAGE_URL_TEMPLATE = "/{0}/{1}/{2}.{3}";
+
+    private static final String DEFAULT_SUFFIX = "jpg";
+
+    private static final String DOCUMENT_SUFFIX = "pdf";
+
+    public static List<String> getSheetUrls(int examId, int campusId, String subjectCode, String examNumber,
+            int count) {
+        List<String> list = new LinkedList<String>();
+        if (StringUtils.isNotEmpty(subjectCode) && count > 0) {
+            for (int i = 1; i <= count; i++) {
+                list.add(MessageFormat.format(SHEET_URL_TEMPLATE, String.valueOf(examId), String.valueOf(campusId),
+                        subjectCode, examNumber, String.valueOf(i), DEFAULT_SUFFIX));
+            }
+        }
+        return list;
+    }
+
+    public static String getSheetUrl(int examId, int campusId, String subjectCode, String examNumber, int index) {
+        return MessageFormat.format(SHEET_URL_TEMPLATE, String.valueOf(examId), String.valueOf(campusId), subjectCode,
+                examNumber, String.valueOf(index), DEFAULT_SUFFIX);
+    }
+
+    public static List<String> getSliceUrls(int examId, int campusId, String subjectCode, String examNumber, int start,
+            int count) {
+        List<String> list = new LinkedList<String>();
+        if (StringUtils.isNotEmpty(subjectCode) && count > 0) {
+            for (int i = start; i <= count; i++) {
+                list.add(MessageFormat.format(SLICE_URL_TEMPLATE, String.valueOf(examId), String.valueOf(campusId),
+                        subjectCode, examNumber, String.valueOf(i), DEFAULT_SUFFIX));
+            }
+        }
+        return list;
+    }
+
+    public static String getSliceUrl(int examId, int campusId, String subjectCode, String examNumber, int index) {
+        return MessageFormat.format(SLICE_URL_TEMPLATE, String.valueOf(examId), String.valueOf(campusId), subjectCode,
+                examNumber, String.valueOf(index), DEFAULT_SUFFIX);
+    }
+
+    public static String getAnswerUrl(int examId, String subjectCode) {
+        return MessageFormat.format(ANSWER_URL_TEMPLATE, String.valueOf(examId), subjectCode, DOCUMENT_SUFFIX);
+    }
+
+    public static String getPaperUrl(int examId, String subjectCode) {
+        return MessageFormat.format(PAPER_URL_TEMPLATE, String.valueOf(examId), subjectCode, DOCUMENT_SUFFIX);
+    }
+
+    public static List<String> getPackageUrls(int examId, String packageCode, int count) {
+        List<String> list = new LinkedList<String>();
+        if (StringUtils.isNotEmpty(packageCode) && count > 0) {
+            for (int i = 1; i <= count; i++) {
+                list.add(MessageFormat.format(PACKAGE_URL_TEMPLATE, String.valueOf(examId), packageCode,
+                        String.valueOf(i), DEFAULT_SUFFIX));
+            }
+        }
+        return list;
+    }
+}

+ 11 - 7
stmms-web/src/main/java/cn/com/qmth/stmms/api/controller/ExamStudentController.java

@@ -6,9 +6,6 @@ import java.util.List;
 
 import javax.servlet.http.HttpServletRequest;
 
-import net.sf.json.JSONArray;
-import net.sf.json.JSONObject;
-
 import org.apache.commons.lang.StringUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -48,6 +45,8 @@ import cn.com.qmth.stmms.biz.user.model.User;
 import cn.com.qmth.stmms.biz.utils.ScoreItem;
 import cn.com.qmth.stmms.common.enums.LibraryStatus;
 import cn.com.qmth.stmms.common.utils.RequestUtils;
+import net.sf.json.JSONArray;
+import net.sf.json.JSONObject;
 
 @Controller("examStudentApiController")
 @RequestMapping("/api")
@@ -75,7 +74,7 @@ public class ExamStudentController extends BaseApiController {
 
     @Autowired
     private ExamStudentPaperService studentPaperService;
-    
+
     @Autowired
     private MarkerService markerService;
 
@@ -205,7 +204,8 @@ public class ExamStudentController extends BaseApiController {
     @RequestMapping("/exam/students")
     @ResponseBody
     public JSONArray getStudent(HttpServletRequest request, ExamStudentSearchQuery query,
-            @RequestParam(required = false) Boolean withScoreDetail) {
+            @RequestParam(required = false) Boolean withScoreDetail,
+            @RequestParam(required = false) Boolean withMarkTrack) {
         User user = RequestUtils.getApiUser(request);
         JSONArray array = new JSONArray();
         if (query.getExamId() == null) {
@@ -245,8 +245,8 @@ public class ExamStudentController extends BaseApiController {
                 obj.accumulate("campusCode", campus != null ? campus.getId().toString() : "");
                 Marker marker = markerService.findByStudentId(student.getId());
                 String markerName = "";
-                if(marker != null){
-                	markerName = marker.getName();
+                if (marker != null) {
+                    markerName = marker.getName();
                 }
                 try {
                     if (withScoreDetail != null && withScoreDetail.booleanValue()) {
@@ -298,6 +298,10 @@ public class ExamStudentController extends BaseApiController {
                         }
                         obj.accumulate("subjectiveScoreDetail", subjective);
                     }
+                    // 返回评卷标记
+                    if (withMarkTrack != null && withMarkTrack.booleanValue()) {
+                        obj.accumulate("tags", examStudentService.buildSheetTags(student));
+                    }
                     array.add(obj);
                 } catch (Exception e) {
                     logger.error("student api error", e);

+ 105 - 0
stmms-web/src/main/java/cn/com/qmth/stmms/api/controller/PictureController.java

@@ -0,0 +1,105 @@
+package cn.com.qmth.stmms.api.controller;
+
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.List;
+
+import javax.imageio.ImageIO;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import cn.com.qmth.stmms.admin.utils.UpyunConfig;
+import cn.com.qmth.stmms.api.utils.ImageBuildUtil;
+import cn.com.qmth.stmms.biz.campus.model.Campus;
+import cn.com.qmth.stmms.biz.campus.service.CampusService;
+import cn.com.qmth.stmms.biz.exam.model.ExamStudent;
+import cn.com.qmth.stmms.biz.exam.service.ExamStudentService;
+import cn.com.qmth.stmms.biz.utils.PictureTag;
+import cn.com.qmth.stmms.common.upyun.UpYun;
+import cn.com.qmth.stmms.common.utils.PictureUrlBuilder;
+
+@Controller("pictureController")
+@RequestMapping("/")
+public class PictureController {
+
+    protected static final Logger log = LoggerFactory.getLogger(PictureController.class);
+
+    @Autowired
+    private CampusService campusService;
+
+    @Autowired
+    private ExamStudentService studentService;
+
+    @Value("${file.root}")
+    private String baseDir;
+
+    @Autowired
+    private UpyunConfig config;
+
+    @RequestMapping("/sheet/{examId}/{examNumber}-{index}")
+    public void getSheet(HttpServletResponse response, @PathVariable Integer examId, @PathVariable String examNumber,
+            @PathVariable Integer index, @RequestParam(required = false, defaultValue = "true") Boolean withTag)
+            throws IOException {
+        ExamStudent student = studentService.findByExamIdAndExamNumber(examId, examNumber);
+        if (student == null || !student.isUpload() || index < 1 || index > student.getSheetCount()) {
+            response.sendError(HttpStatus.NOT_FOUND.value());
+            return;
+        }
+        try {
+            List<PictureTag> tags = null;
+            if (withTag != null && withTag) {
+                tags = studentService.buildSheetTags(student, index);
+            }
+            BufferedImage image = ImageBuildUtil.buildTagImage(getSheetImage(student, index), tags);
+            response.setContentType("image/jpeg");
+            ImageIO.write(image, "jpg", response.getOutputStream());
+        } catch (Exception e) {
+            log.error("get sheet error", e);
+            response.reset();
+            response.sendError(HttpStatus.NOT_FOUND.value());
+            return;
+        }
+    }
+
+    public BufferedImage getSheetImage(ExamStudent student, int index) throws FileNotFoundException, IOException {
+        Campus campus = campusService.findBySchoolAndName(student.getSchoolId(), student.getCampusName());
+        String url = PictureUrlBuilder.getSheetUrl(student.getExamId(), campus.getId(), student.getSubjectCode(),
+                student.getExamNumber(), index);
+        if (StringUtils.isNotBlank(baseDir)) {
+            return ImageIO.read(new File(new File(baseDir, config.getSheetBucket()), url));
+        } else {
+            UpYun upyun = new UpYun(config.getSheetBucket(), config.getSheetUsername(), config.getSheetPassword());
+            ByteArrayOutputStream ous = new ByteArrayOutputStream();
+            upyun.readFile(url, ous);
+            return ImageIO.read(new ByteArrayInputStream(ous.toByteArray()));
+        }
+    }
+
+    public BufferedImage getSliceImage(ExamStudent student, int index) throws FileNotFoundException, IOException {
+        Campus campus = campusService.findBySchoolAndName(student.getSchoolId(), student.getCampusName());
+        String url = PictureUrlBuilder.getSliceUrl(student.getExamId(), campus.getId(), student.getSubjectCode(),
+                student.getExamNumber(), index);
+        if (StringUtils.isNotBlank(baseDir)) {
+            return ImageIO.read(new File(new File(baseDir, config.getSliceBucket()), url));
+        } else {
+            UpYun upyun = new UpYun(config.getSliceBucket(), config.getSliceUsername(), config.getSlicePassword());
+            ByteArrayOutputStream ous = new ByteArrayOutputStream();
+            upyun.readFile(url, ous);
+            return ImageIO.read(new ByteArrayInputStream(ous.toByteArray()));
+        }
+    }
+}

+ 55 - 0
stmms-web/src/main/java/cn/com/qmth/stmms/api/utils/ImageBuildUtil.java

@@ -0,0 +1,55 @@
+package cn.com.qmth.stmms.api.utils;
+
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.List;
+
+import cn.com.qmth.stmms.biz.utils.PictureTag;
+
+public class ImageBuildUtil {
+
+    public static final String DEFAULT_FONT_NAME = "Arial";
+
+    public static final int DEFAULT_FONT_SIZE = 60;
+
+    public static final int DEFAULT_LINE_GAP = 10;
+
+    /**
+     * 根据指定的标记内容绘制新的图片
+     * 
+     * @param image
+     * @param tags
+     * @return
+     * @throws FileNotFoundException
+     * @throws IOException
+     */
+    public static BufferedImage buildTagImage(BufferedImage image, List<PictureTag> tags)
+            throws FileNotFoundException, IOException {
+        if (tags != null && !tags.isEmpty()) {
+            BufferedImage newImg = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB);
+            Graphics2D g = newImg.createGraphics();
+            g.drawImage(image, 0, 0, image.getWidth(), image.getHeight(), null);
+            g.setColor(Color.RED);
+            for (PictureTag tag : tags) {
+                if (tag.getContent() == null) {
+                    continue;
+                }
+                int fontSize = tag.getSize() > 0 ? tag.getSize() : DEFAULT_FONT_SIZE;
+                int left = Math.max(fontSize, tag.getLeft());
+                int top = Math.max(fontSize, tag.getTop());
+                g.setFont(new Font(DEFAULT_FONT_NAME, Font.PLAIN, fontSize));
+                for (String content : tag.getContent()) {
+                    g.drawString(content, left, top);
+                    top = top + fontSize + DEFAULT_LINE_GAP;
+                }
+            }
+            g.dispose();
+            image = newImg;
+        }
+        return image;
+    }
+}

+ 64 - 64
stmms-web/src/main/webapp/WEB-INF/application.properties

@@ -1,64 +1,64 @@
-#jdbc config
-driverClassName=com.mysql.jdbc.Driver
-#jdbc config
-url=jdbc:mysql://192.168.10.30:3306/stmms_gx?useUnicode=true&characterEncoding=UTF-8
-username=root
-password=root
-
-#cookie config
-cookie.max.age=36000
-cookie.domain=
-cookie.path=/
-
-#server config
-slice.image.server=http://gx-slice.markingcloud.com
-sheet.image.server=http://gx-sheet.markingcloud.com
-package.image.server=http://gx-package.markingcloud.com
-card.server=http://gx-card.markingcloud.com
-##slice.image.server=http://${local.ip}:9000/gx-slice
-##sheet.image.server=http://${local.ip}:9000/gx-sheet
-##package.image.server=http://${local.ip}:9000/gx-package
-##card.server=http://${local.ip}:9000/gx-card
-
-#redis switch
-use.redis=false
-
-#redis config
-redis.pool.maxTotal=100
-redis.pool.maxIdle=100
-redis.pool.testOnBorrow=true
-redis.ip=mc-redis1
-redis.port=6379
-
-#redis cache config
-cache.redis.pool.maxTotal=50
-cache.redis.pool.maxIdle=50
-cache.redis.pool.testOnBorrow=true
-cache.redis.ip=mc-redis2
-cache.redis.port=6379
-
-##file root path
-file.root=
-
-##upyun image config
-upyun.sheet.bucket=gx-sheet
-upyun.sheet.username=qmth-picture
-upyun.sheet.password=qmth12345678
-
-upyun.slice.bucket=gx-slice
-upyun.slice.username=qmth-picture
-upyun.slice.password=qmth12345678
-
-##\u662f\u5426\u66ff\u6362\u79d1\u76ee\u540d\u79f0\u4e2d\u7684\u534a\u89d2\u62ec\u53f7
-subject.name.bracket.replace=false
-jvm.cleanMapinterval=20
-jvm.cleanMapTimer=0 0/10 6-23 * * ?
-app.home=
-#value = aopeng or null (aopeng set 'aopeng' others set '')
-app.index=
-
-# value = true or false (aopeng set true and others set false.If a project needs to be set up true in the future else false)
-scoreList.showExportScore=false
-
-#value = true or false  (aopeng set true and others set false.If a project needs to be set up true in the future else false)
-marker.showBtnImportAndBtnUpdateImport=false
+#jdbc config
+driverClassName=com.mysql.jdbc.Driver
+#jdbc config
+url=jdbc:mysql://localhost:3306/stmms_gx?useUnicode=true&characterEncoding=UTF-8
+username=root
+password=root
+
+#cookie config
+cookie.max.age=36000
+cookie.domain=
+cookie.path=/
+
+#server config
+slice.image.server=https://gx-slice.markingcloud.com
+sheet.image.server=https://gx-sheet.markingcloud.com
+package.image.server=http://gx-package.markingcloud.com
+card.server=http://gx-card.markingcloud.com
+##slice.image.server=http://${local.ip}:9000/gx-slice
+##sheet.image.server=http://${local.ip}:9000/gx-sheet
+##package.image.server=http://${local.ip}:9000/gx-package
+##card.server=http://${local.ip}:9000/gx-card
+
+#redis switch
+use.redis=false
+
+#redis config
+redis.pool.maxTotal=100
+redis.pool.maxIdle=100
+redis.pool.testOnBorrow=true
+redis.ip=mc-redis1
+redis.port=6379
+
+#redis cache config
+cache.redis.pool.maxTotal=50
+cache.redis.pool.maxIdle=50
+cache.redis.pool.testOnBorrow=true
+cache.redis.ip=mc-redis2
+cache.redis.port=6379
+
+##file root path
+file.root=
+
+##upyun image config
+upyun.sheet.bucket=gx-sheet
+upyun.sheet.username=qmth-picture
+upyun.sheet.password=qmth12345678
+
+upyun.slice.bucket=gx-slice
+upyun.slice.username=qmth-picture
+upyun.slice.password=qmth12345678
+
+##\u662f\u5426\u66ff\u6362\u79d1\u76ee\u540d\u79f0\u4e2d\u7684\u534a\u89d2\u62ec\u53f7
+subject.name.bracket.replace=false
+jvm.cleanMapinterval=20
+jvm.cleanMapTimer=0 0/10 6-23 * * ?
+app.home=
+#value = aopeng or null (aopeng set 'aopeng' others set '')
+app.index=
+
+# value = true or false (aopeng set true and others set false.If a project needs to be set up true in the future else false)
+scoreList.showExportScore=false
+
+#value = true or false  (aopeng set true and others set false.If a project needs to be set up true in the future else false)
+marker.showBtnImportAndBtnUpdateImport=false