Quellcode durchsuchen

Initial commit or whatever message you want

root vor 2 Tagen
Ursprung
Commit
89097e9617
87 geänderte Dateien mit 8124 neuen und 0 gelöschten Zeilen
  1. BIN
      .DS_Store
  2. BIN
      .deploy.sh.swp
  3. 4 0
      .gitignore
  4. 11 0
      Dockerfile
  5. 19 0
      build.sh
  6. 117 0
      cache/redis.go
  7. BIN
      controller/.DS_Store
  8. 47 0
      controller/callback/callback.go
  9. 126 0
      controller/questionnaire_subject/questionnaire_subject.go
  10. 312 0
      controller/questionnaire_survey/survey.go
  11. 237 0
      controller/questionnaire_template/questionnaire_template.go
  12. 149 0
      controller/survey/manage.go
  13. 63 0
      controller/survey_mechanism/external.go
  14. 103 0
      controller/survey_mechanism/manage.go
  15. 136 0
      controller/survey_mechanism/mechanism.go
  16. 101 0
      controller/survey_mechanism/member.go
  17. 37 0
      controller/survey_result/external.go
  18. 79 0
      controller/survey_result/manage.go
  19. 65 0
      controller/survey_result/mechanism.go
  20. 222 0
      controller/survey_result/member.go
  21. 72 0
      deploy.sh
  22. 56 0
      middleware/authorize.go
  23. 56 0
      middleware/authorize_mechanism.go
  24. 106 0
      middleware/authorize_mechanism_by_appkey.go
  25. 59 0
      middleware/authorize_member.go
  26. 32 0
      middleware/authorize_member_omitempty.go
  27. 33 0
      middleware/authorize_survey_token.go
  28. 23 0
      middleware/cors.go
  29. 23 0
      middleware/member_mechanism.go
  30. 28 0
      middleware/permission_check.go
  31. 28 0
      middleware/permission_check_staff.go
  32. BIN
      model/.DS_Store
  33. 108 0
      model/migration.go
  34. 24 0
      model/questionnaire_subject.go
  35. 19 0
      model/questionnaire_survey_questionnaire_subject.go
  36. 41 0
      model/questionnaire_suvey.go
  37. 22 0
      model/questionnaire_template.go
  38. 18 0
      model/questionnaire_template_subject.go
  39. 56 0
      model/survey.go
  40. 61 0
      model/survey_mechanism.go
  41. 63 0
      model/survey_result.go
  42. 41 0
      model/system_setting.go
  43. 35 0
      model/types/types.go
  44. 67 0
      response/errcode.go
  45. 76 0
      response/response.go
  46. 164 0
      router/router.go
  47. 291 0
      sdk/survey_disease/disease_screening.go
  48. 749 0
      sdk/survey_disease/structs.go
  49. BIN
      service/.DS_Store
  50. 192 0
      service/questionnaire_subject/questionnaire_subject.go
  51. 257 0
      service/questionnaire_survey/survey.go
  52. 112 0
      service/questionnaire_survey/survey_subject.go
  53. 194 0
      service/questionnaire_template/questionnaire_template.go
  54. 100 0
      service/questionnaire_template/questionnaire_template_subject.go
  55. 1 0
      service/structs.go
  56. 243 0
      service/survey/survey.go
  57. 38 0
      service/survey/survey_test.go
  58. 242 0
      service/survey_import/manage.go
  59. 21 0
      service/survey_mechanism/external.go
  60. 157 0
      service/survey_mechanism/mechanism.go
  61. 133 0
      service/survey_mechanism/member.go
  62. 214 0
      service/survey_mechanism/survey_mechanism.go
  63. 12 0
      service/survey_mechanism/survey_mechanism_test.go
  64. 325 0
      service/survey_result/member.go
  65. 372 0
      service/survey_result/survey_result.go
  66. 12 0
      service/survey_result/survey_result_test.go
  67. 89 0
      service/survey_token/survey_token.go
  68. 43 0
      service/system_setting/manage.go
  69. 14 0
      service/system_setting/manage_test.go
  70. BIN
      tests/.DS_Store
  71. 23 0
      tests/main.go
  72. BIN
      util/.DS_Store
  73. 133 0
      util/algor/main.go
  74. 21 0
      util/algor/main_test.go
  75. 70 0
      util/algor/structs.go
  76. 51 0
      util/aliyun/dysms.go
  77. 11 0
      util/constants/constants.go
  78. 1 0
      util/log/log.go
  79. 73 0
      util/rabbitmq/index.go
  80. 5 0
      util/rabbitmq/logger.go
  81. 103 0
      util/rabbitmq/producer.go
  82. 177 0
      util/rabbitmq/rabbitmq.go
  83. 82 0
      util/rabbitmq/rabbitmq_test.go
  84. 238 0
      util/util.go
  85. 56 0
      util/validator/validator.go
  86. 63 0
      validators/questionnaire.go
  87. 67 0
      validators/survey.go

BIN
.DS_Store


BIN
.deploy.sh.swp


+ 4 - 0
.gitignore

@@ -0,0 +1,4 @@
+.vscode/*
+.env*
+vendor/
+/app

+ 11 - 0
Dockerfile

@@ -0,0 +1,11 @@
+FROM docker.1ms.run/alpine:3.16
+
+RUN mkdir /src
+WORKDIR /src
+RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
+RUN apk add tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
+    && echo "Asia/Shanghai" > /etc/timezone \
+    && apk del tzdata
+ADD . /src
+
+# ENTRYPOINT [ "/task_service" ]

+ 19 - 0
build.sh

@@ -0,0 +1,19 @@
+# Pre Set
+set -x
+
+# Set ENV
+export GO111MODULE=on                               # Go1.11.x 版本需要开启
+export GOPROXY=https://goproxy.cn
+export GOPRIVATE=gogs.uu.mdfitnesscao.com
+
+# Set Var
+Src=./                                             # 程序源码
+Out="app"
+Options="-a -installsuffix cgo -o"
+PreSet='GOOS=linux GOARCH=amd64'                  # 构建变量设置
+
+# Build
+go version
+go env
+
+GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -tags=jsoniter -o ${Out} ${Src}             # build

+ 117 - 0
cache/redis.go

@@ -0,0 +1,117 @@
+package cache
+
+import (
+	"context"
+	"fmt"
+	"log"
+	"os"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/go-redis/redis/v8"
+
+	jsoniter "github.com/json-iterator/go"
+)
+
+var json = jsoniter.ConfigCompatibleWithStandardLibrary
+
+type Cache struct {
+	client *redis.Client
+	prefix string
+}
+
+type ChannelUserCache struct {
+	ID               int64
+	Name             string
+	Permission       []string `json:"permission"`
+	IsTemp           bool     `json:"isTemp"`
+	ValidArchivesIds []string `json:"validArchivesIds"`
+}
+
+type ManagerCache struct {
+	ID int64
+}
+
+type MemberCache struct {
+	ID   int64
+	Name string
+}
+
+var cache = &Cache{}
+
+func Instance() *Cache {
+	return cache
+}
+
+func GetClient() *redis.Client {
+	return cache.client
+}
+
+func InitRedis() *redis.Client {
+	RedisDB, err := strconv.Atoi(os.Getenv("REDIS_DB"))
+	if err != nil {
+		log.Panic("Redis数据库错误", err)
+	}
+	// 初始化Cache
+	return InitRedisClient(fmt.Sprintf("%s:%s", os.Getenv("REDIS_HOST"), os.Getenv("REDIS_PORT")), os.Getenv("REDIS_PASSWORD"), "lumen_cache:", RedisDB)
+}
+
+func InitRedisClient(addr, password, p string, db int) *redis.Client {
+	client := redis.NewClient(&redis.Options{
+		Addr:     addr,
+		Password: password,
+		DB:       db,
+	})
+
+	if err := client.Ping(context.Background()).Err(); err != nil {
+		log.Panic("start cache", err)
+	}
+	cache.prefix = p
+	cache.client = client
+	return client
+}
+
+func (c *Cache) KeyWithPrefix(key string) string {
+	return c.prefix + key
+}
+
+func (c *Cache) Get(key string) (string, error) {
+	value, err := c.client.Get(context.Background(), c.KeyWithPrefix(key)).Result()
+	if err != nil {
+		return "", err
+	}
+	// PHP序列化之后的对象无法直接转为go的数据结构,直接都当数组处理
+	value = strings.ReplaceAll(value, `O:8:"stdClass"`, `a`)
+	value = strings.ReplaceAll(value, `O:17:"App\Models\Member"`, `a`)
+	value = strings.ReplaceAll(value, `O:15:"App\Models\User"`, `a`)
+	return value, nil
+}
+
+func (c *Cache) Put(key string, value interface{}, expiration time.Duration) error {
+	valueStr, err := json.MarshalToString(value)
+	if err != nil {
+		return err
+	}
+	ok, err := c.client.SetEX(context.Background(), c.KeyWithPrefix(key), valueStr, expiration).Result()
+	fmt.Println(ok, err)
+	return err
+}
+func (c *Cache) PutStrForever(key string, value string) error {
+	_, err := c.client.Set(context.Background(), c.KeyWithPrefix(key), value, 0).Result()
+	return err
+}
+
+func (c *Cache) PutStr(key string, value string, expiration time.Duration) error {
+	ok, err := c.client.SetEX(context.Background(), c.KeyWithPrefix(key), value, expiration).Result()
+	fmt.Println(ok, err)
+	return err
+}
+
+func (c *Cache) Delete(key string) (int64, error) {
+	return c.client.Del(context.Background(), c.KeyWithPrefix(key)).Result()
+}
+
+func (c *Cache) Incr(key string) (int64, error) {
+	return c.client.Incr(context.Background(), c.KeyWithPrefix(key)).Result()
+}

BIN
controller/.DS_Store


+ 47 - 0
controller/callback/callback.go

@@ -0,0 +1,47 @@
+package callback
+
+import (
+	"fmt"
+	"net/http"
+	"surveyService/service/survey_import"
+	"surveyService/service/survey_result"
+
+	"github.com/gin-gonic/gin"
+)
+
+func CallbackSurveyResult(c *gin.Context) {
+
+	// 获取请求的JSON内容
+	type Validator struct {
+		SN    string         `json:"sn" form:"sn" binding:"required"`
+		Extra string         `json:"extra" form:"extra" binding:"omitempty"`
+		Data  map[string]any `json:"data" form:"data" binding:"required"`
+		Type  int            `json:"type" form:"type" binding:"required"`
+	}
+	var validator Validator
+	if err := c.ShouldBind(&validator); err != nil {
+		c.String(http.StatusOK, "数据格式校验失败")
+		return
+	}
+
+	receiveErr := survey_result.ReceiveResult(validator.SN, validator.Extra, validator.Type, validator.Data)
+	if receiveErr != nil {
+		c.String(http.StatusOK, fmt.Sprintf("数据录入失败: %s", receiveErr.Msg))
+		return
+	}
+
+	c.String(http.StatusOK, "success")
+}
+
+func CallbackSurveySync(c *gin.Context) {
+	// 获取请求的JSON内容
+	var validator survey_import.SyncData
+	if err := c.ShouldBind(&validator); err != nil {
+		c.String(http.StatusOK, "数据格式校验失败")
+		return
+	}
+
+	survey_import.StartSync(validator)
+
+	c.String(http.StatusOK, "success")
+}

+ 126 - 0
controller/questionnaire_subject/questionnaire_subject.go

@@ -0,0 +1,126 @@
+package questionnaire_subject
+
+import (
+	"surveyService/response"
+	"surveyService/service/questionnaire_subject"
+	"surveyService/validators"
+
+	"github.com/gin-gonic/gin"
+)
+
+// 修改或创建
+func UpdateOrCreate(c *gin.Context) {
+	var validator validators.QuestionnaireSubject
+	err := c.ShouldBind(&validator)
+	if err != nil {
+		response.FailValidator(c, err)
+		return
+	}
+	var updateErr *response.ErrCode
+	if validator.ID > 0 {
+		updateErr = questionnaire_subject.Update(&validator)
+	} else {
+		updateErr = questionnaire_subject.Create(&validator)
+	}
+
+	if updateErr != nil {
+		response.Fail(c, updateErr)
+		return
+	}
+
+	response.Success(c, map[string]any{})
+}
+
+// 分页获取列表
+func Paginate(c *gin.Context) {
+	type Validator struct {
+		Page     int    `json:"page" form:"page" binding:"omitempty,min=1"`
+		PageSize int    `json:"pageSize" form:"pageSize" binding:"omitempty,min=1,max=100"`
+		Key      string `json:"key" form:"key" binding:"omitempty"`
+	}
+	var validator Validator
+	err := c.ShouldBindQuery(&validator)
+	if err != nil {
+		response.FailValidator(c, err)
+		return
+	}
+
+	subjects, total := questionnaire_subject.Paginate(validator.Page, validator.PageSize, validator.Key)
+
+	var list []*validators.QuestionnaireSubject = make([]*validators.QuestionnaireSubject, 0)
+	for _, item := range subjects {
+		list = append(list, questionnaire_subject.Format(item))
+	}
+
+	response.Success(c, map[string]any{
+		"list":  list,
+		"total": total,
+	})
+}
+
+// 获取全部列表
+func List(c *gin.Context) {
+	type Validator struct {
+		Key string `json:"key" form:"key" binding:"omitempty"`
+	}
+	var validator Validator
+	err := c.ShouldBindQuery(&validator)
+	if err != nil {
+		response.FailValidator(c, err)
+		return
+	}
+
+	subjects := questionnaire_subject.List(validator.Key)
+
+	var list []*validators.QuestionnaireSubject = make([]*validators.QuestionnaireSubject, 0)
+	for _, item := range subjects {
+		list = append(list, questionnaire_subject.Format(item))
+	}
+
+	response.Success(c, map[string]any{
+		"list": list,
+	})
+}
+
+// 删除
+func Delete(c *gin.Context) {
+	type Validator struct {
+		ID int64 `json:"id" form:"id" binding:"required"`
+	}
+	var validator Validator
+	err := c.ShouldBind(&validator)
+	if err != nil {
+		response.FailValidator(c, err)
+		return
+	}
+
+	deleteErr := questionnaire_subject.Delete(validator.ID)
+	if deleteErr != nil {
+		response.Fail(c, deleteErr)
+		return
+	}
+
+	response.Success(c, map[string]any{})
+}
+
+// 修改备注
+func UpdateMark(c *gin.Context) {
+	type Validator struct {
+		ID   int64  `json:"id" form:"id" binding:"required"`
+		Mark string `json:"mark" form:"mark" binding:"omitempty"`
+	}
+	var validator Validator
+	err := c.ShouldBind(&validator)
+	if err != nil {
+		response.FailValidator(c, err)
+		return
+	}
+
+	updateErr := questionnaire_subject.UpdateMark(validator.ID, validator.Mark)
+	if updateErr != nil {
+		response.Fail(c, updateErr)
+		return
+	}
+
+	response.Success(c, map[string]any{})
+}

+ 312 - 0
controller/questionnaire_survey/survey.go

@@ -0,0 +1,312 @@
+package questionnaire_survey
+
+import (
+	"surveyService/response"
+	"surveyService/service/questionnaire_survey"
+	"surveyService/service/survey_import"
+	"surveyService/validators"
+
+	"github.com/gin-gonic/gin"
+)
+
+// 修改或创建
+func UpdateOrCreate(c *gin.Context) {
+	var validator validators.QuestionnaireSurvey
+	err := c.ShouldBind(&validator)
+	if err != nil {
+		response.FailValidator(c, err)
+		return
+	}
+	var updateErr *response.ErrCode
+	if validator.ID > 0 {
+		updateErr = questionnaire_survey.Update(&validator)
+	} else {
+		updateErr = questionnaire_survey.Create(&validator)
+	}
+
+	if updateErr != nil {
+		response.Fail(c, updateErr)
+		return
+	}
+
+	response.Success(c, map[string]any{})
+}
+
+// 分页获取列表
+func Paginate(c *gin.Context) {
+	type Validator struct {
+		Page     int    `json:"page" form:"page" binding:"omitempty,min=1"`
+		PageSize int    `json:"pageSize" form:"pageSize" binding:"omitempty,min=1,max=100"`
+		Key      string `json:"key" form:"key" binding:"omitempty"`
+	}
+	var validator Validator
+	err := c.ShouldBindQuery(&validator)
+	if err != nil {
+		response.FailValidator(c, err)
+		return
+	}
+
+	surveys, total := questionnaire_survey.Paginate(validator.Page, validator.PageSize, validator.Key)
+
+	var list []*validators.QuestionnaireSurvey = make([]*validators.QuestionnaireSurvey, 0)
+	for _, item := range surveys {
+		list = append(list, questionnaire_survey.Format(item))
+	}
+
+	response.Success(c, map[string]any{
+		"list":  list,
+		"total": total,
+	})
+}
+
+// 获取单个
+func Detail(c *gin.Context) {
+	type Validator struct {
+		ID int64 `json:"id" form:"id" binding:"required,min=1"`
+	}
+	var validator Validator
+	err := c.ShouldBindQuery(&validator)
+	if err != nil {
+		response.FailValidator(c, err)
+		return
+	}
+
+	surveyDetail, findErr := questionnaire_survey.Find(validator.ID)
+	if findErr != nil {
+		response.Fail(c, findErr)
+		return
+	}
+
+	response.Success(c, map[string]any{
+		"detail": questionnaire_survey.Format(surveyDetail),
+	})
+}
+
+// 获取全部列表
+func List(c *gin.Context) {
+	type Validator struct {
+		Key string `json:"key" form:"key" binding:"omitempty"`
+	}
+	var validator Validator
+	err := c.ShouldBindQuery(&validator)
+	if err != nil {
+		response.FailValidator(c, err)
+		return
+	}
+
+	surveys := questionnaire_survey.List(validator.Key)
+
+	var list []*validators.QuestionnaireSurvey = make([]*validators.QuestionnaireSurvey, 0)
+	for _, item := range surveys {
+		list = append(list, questionnaire_survey.Format(item))
+	}
+
+	response.Success(c, map[string]any{
+		"list": list,
+	})
+}
+
+// 删除
+func Delete(c *gin.Context) {
+	type Validator struct {
+		ID int64 `json:"id" form:"id" binding:"required"`
+	}
+	var validator Validator
+	err := c.ShouldBind(&validator)
+	if err != nil {
+		response.FailValidator(c, err)
+		return
+	}
+
+	deleteErr := questionnaire_survey.Delete(validator.ID)
+	if deleteErr != nil {
+		response.Fail(c, deleteErr)
+		return
+	}
+
+	response.Success(c, map[string]any{})
+}
+
+// 修改PEG.js
+func UpdatePeg(c *gin.Context) {
+	type Validator struct {
+		ID  int64  `json:"id" form:"id" binding:"required"`
+		Peg string `json:"peg" form:"peg" binding:"required"`
+	}
+	var validator Validator
+	err := c.ShouldBind(&validator)
+	if err != nil {
+		response.FailValidator(c, err)
+		return
+	}
+
+	updateErr := questionnaire_survey.UpdatePeg(validator.ID, validator.Peg)
+	if updateErr != nil {
+		response.Fail(c, updateErr)
+		return
+	}
+
+	response.Success(c, map[string]any{})
+}
+
+// 修改备注
+func UpdateRemark(c *gin.Context) {
+	type Validator struct {
+		ID     int64  `json:"id" form:"id" binding:"required"`
+		Remark string `json:"remark" form:"remark" binding:"required"`
+	}
+	var validator Validator
+	err := c.ShouldBind(&validator)
+	if err != nil {
+		response.FailValidator(c, err)
+		return
+	}
+
+	updateErr := questionnaire_survey.UpdateRemark(validator.ID, validator.Remark)
+	if updateErr != nil {
+		response.Fail(c, updateErr)
+		return
+	}
+
+	response.Success(c, map[string]any{})
+}
+
+// 修改状态
+func UpdateStatus(c *gin.Context) {
+	type Validator struct {
+		ID     int64 `json:"id" form:"id" binding:"required"`
+		Status int   `json:"status" form:"status" binding:"required"`
+	}
+	var validator Validator
+	err := c.ShouldBind(&validator)
+	if err != nil {
+		response.FailValidator(c, err)
+		return
+	}
+
+	updateErr := questionnaire_survey.UpdateStatus(validator.ID, validator.Status)
+	if updateErr != nil {
+		response.Fail(c, updateErr)
+		return
+	}
+
+	response.Success(c, map[string]any{})
+}
+
+// 获取关联的问题库列表
+func ListQuestionnaireSubject(c *gin.Context) {
+	type Validator struct {
+		ID int64 `json:"id" form:"id" binding:"required"`
+	}
+	var validator Validator
+	err := c.ShouldBind(&validator)
+	if err != nil {
+		response.FailValidator(c, err)
+		return
+	}
+
+	subjects := questionnaire_survey.ListSubjects(validator.ID)
+	var list []*validators.SurveyQuestionnaireSubject
+	for _, subject := range subjects {
+		list = append(list, questionnaire_survey.FormatSurveySubject(subject))
+	}
+
+	response.Success(c, map[string]any{
+		"list": list,
+	})
+}
+
+// 批量添加关联的问题库
+func AddQuestionnaireSubject(c *gin.Context) {
+	type Validator struct {
+		ID         int64   `json:"id" form:"id" binding:"required"`
+		SubjectIds []int64 `json:"subjectIds" form:"subjectIds" binding:"required"`
+	}
+	var validator Validator
+	err := c.ShouldBind(&validator)
+	if err != nil {
+		response.FailValidator(c, err)
+		return
+	}
+
+	addErr := questionnaire_survey.AddSubject(validator.ID, validator.SubjectIds)
+	if addErr != nil {
+		response.Fail(c, addErr)
+		return
+	}
+
+	response.Success(c, map[string]any{})
+}
+
+// 移除关联的问题库
+func DeleteQuestionnaireSubject(c *gin.Context) {
+	type Validator struct {
+		ID         int64   `json:"id" form:"id" binding:"required"`
+		SubjectIds []int64 `json:"subjectIds" form:"subjectIds" binding:"required"`
+	}
+	var validator Validator
+	err := c.ShouldBind(&validator)
+	if err != nil {
+		response.FailValidator(c, err)
+		return
+	}
+
+	removeErr := questionnaire_survey.DeleteSubject(validator.ID, validator.SubjectIds)
+	if removeErr != nil {
+		response.Fail(c, removeErr)
+		return
+	}
+
+	response.Success(c, map[string]any{})
+}
+
+// 批量修改关联问题库的排序
+func UpdateQuestionnaireSubjectSort(c *gin.Context) {
+	type Validator struct {
+		ID         int64   `json:"id" form:"id" binding:"required"`
+		SubjectIds []int64 `json:"subjectIds" form:"subjectIds" binding:"required"`
+	}
+	var validator Validator
+	err := c.ShouldBind(&validator)
+	if err != nil {
+		response.FailValidator(c, err)
+		return
+	}
+
+	updateErr := questionnaire_survey.UpdateSort(validator.ID, validator.SubjectIds)
+	if updateErr != nil {
+		response.Fail(c, updateErr)
+		return
+	}
+
+	response.Success(c, map[string]any{})
+}
+
+// 修改关联问题库的是否必填
+func UpdateQuestionnaireSubjectIsRequired(c *gin.Context) {
+	type Validator struct {
+		ID         int64 `json:"id" form:"id" binding:"required"`
+		IsRequired int   `json:"isRequired" form:"isRequired" binding:"required"`
+	}
+	var validator Validator
+	err := c.ShouldBind(&validator)
+	if err != nil {
+		response.FailValidator(c, err)
+		return
+	}
+
+	updateErr := questionnaire_survey.UpdateIsRequired(validator.ID, validator.IsRequired == 1)
+	if updateErr != nil {
+		response.Fail(c, updateErr)
+		return
+	}
+
+	response.Success(c, map[string]any{})
+}
+
+func CacheList(c *gin.Context) {
+
+	survey_import.CacheList()
+
+	response.Success(c, map[string]any{})
+}

+ 237 - 0
controller/questionnaire_template/questionnaire_template.go

@@ -0,0 +1,237 @@
+package questionnaire_template
+
+import (
+	"surveyService/response"
+	"surveyService/service/questionnaire_template"
+	"surveyService/validators"
+
+	"github.com/gin-gonic/gin"
+)
+
+// 修改或创建
+func UpdateOrCreate(c *gin.Context) {
+	var validator validators.QuestionnaireTemplate
+	err := c.ShouldBind(&validator)
+	if err != nil {
+		response.FailValidator(c, err)
+		return
+	}
+	var updateErr *response.ErrCode
+	if validator.ID > 0 {
+		updateErr = questionnaire_template.Update(&validator)
+	} else {
+		updateErr = questionnaire_template.Create(&validator)
+	}
+
+	if updateErr != nil {
+		response.Fail(c, updateErr)
+		return
+	}
+
+	response.Success(c, map[string]any{})
+}
+
+// 分页获取列表
+func Paginate(c *gin.Context) {
+	type Validator struct {
+		Page     int    `json:"page" form:"page" binding:"omitempty,min=1"`
+		PageSize int    `json:"pageSize" form:"pageSize" binding:"omitempty,min=1,max=100"`
+		Key      string `json:"key" form:"key" binding:"omitempty"`
+	}
+	var validator Validator
+	err := c.ShouldBindQuery(&validator)
+	if err != nil {
+		response.FailValidator(c, err)
+		return
+	}
+
+	subjects, total := questionnaire_template.Paginate(validator.Page, validator.PageSize, validator.Key)
+
+	var list []*validators.QuestionnaireTemplate = make([]*validators.QuestionnaireTemplate, 0)
+	for _, item := range subjects {
+		list = append(list, questionnaire_template.Format(item))
+	}
+
+	response.Success(c, map[string]any{
+		"list":  list,
+		"total": total,
+	})
+}
+
+// 分页获取列表
+func Detail(c *gin.Context) {
+	type Validator struct {
+		ID int64 `json:"id" form:"id" binding:"required"`
+	}
+	var validator Validator
+	err := c.ShouldBindQuery(&validator)
+	if err != nil {
+		response.FailValidator(c, err)
+		return
+	}
+
+	subject, errCode := questionnaire_template.Find(validator.ID)
+	if errCode != nil {
+		response.Fail(c, errCode)
+		return
+	}
+	response.Success(c, map[string]any{
+		"detail": questionnaire_template.Format(subject),
+	})
+}
+
+// 获取全部列表
+func List(c *gin.Context) {
+	type Validator struct {
+		Key string `json:"key" form:"key" binding:"omitempty"`
+	}
+	var validator Validator
+	err := c.ShouldBindQuery(&validator)
+	if err != nil {
+		response.FailValidator(c, err)
+		return
+	}
+
+	subjects := questionnaire_template.List(validator.Key)
+
+	var list []*validators.QuestionnaireTemplate = make([]*validators.QuestionnaireTemplate, 0)
+	for _, item := range subjects {
+		list = append(list, questionnaire_template.Format(item))
+	}
+
+	response.Success(c, map[string]any{
+		"list": list,
+	})
+}
+
+// 删除
+func Delete(c *gin.Context) {
+	type Validator struct {
+		ID int64 `json:"id" form:"id" binding:"required"`
+	}
+	var validator Validator
+	err := c.ShouldBind(&validator)
+	if err != nil {
+		response.FailValidator(c, err)
+		return
+	}
+
+	deleteErr := questionnaire_template.Delete(validator.ID)
+	if deleteErr != nil {
+		response.Fail(c, deleteErr)
+		return
+	}
+
+	response.Success(c, map[string]any{})
+}
+
+// 删除
+func UpdatePeg(c *gin.Context) {
+	type Validator struct {
+		ID  int64  `json:"id" form:"id" binding:"required"`
+		Peg string `json:"peg" form:"peg" binding:"required"`
+	}
+	var validator Validator
+	err := c.ShouldBind(&validator)
+	if err != nil {
+		response.FailValidator(c, err)
+		return
+	}
+
+	updateErr := questionnaire_template.UpdatePeg(validator.ID, validator.Peg)
+	if updateErr != nil {
+		response.Fail(c, updateErr)
+		return
+	}
+
+	response.Success(c, map[string]any{})
+}
+
+// 获取关联的问题库列表
+func ListQuestionnaireSubject(c *gin.Context) {
+	type Validator struct {
+		ID int64 `json:"id" form:"id" binding:"required"`
+	}
+	var validator Validator
+	err := c.ShouldBind(&validator)
+	if err != nil {
+		response.FailValidator(c, err)
+		return
+	}
+
+	subjects := questionnaire_template.ListSubjects(validator.ID)
+	var list []*validators.QuestionnaireTemplateSubject
+	for _, subject := range subjects {
+		list = append(list, questionnaire_template.FormatTemplateSubject(subject))
+	}
+
+	response.Success(c, map[string]any{
+		"list": list,
+	})
+}
+
+// 批量添加关联的问题库
+func AddQuestionnaireSubject(c *gin.Context) {
+	type Validator struct {
+		ID         int64   `json:"id" form:"id" binding:"required"`
+		SubjectIds []int64 `json:"subjectIds" form:"subjectIds" binding:"required"`
+	}
+	var validator Validator
+	err := c.ShouldBind(&validator)
+	if err != nil {
+		response.FailValidator(c, err)
+		return
+	}
+
+	addErr := questionnaire_template.AddSubject(validator.ID, validator.SubjectIds)
+	if addErr != nil {
+		response.Fail(c, addErr)
+		return
+	}
+
+	response.Success(c, map[string]any{})
+}
+
+// 移除关联的问题库
+func DeleteQuestionnaireSubject(c *gin.Context) {
+	type Validator struct {
+		ID         int64   `json:"id" form:"id" binding:"required"`
+		SubjectIds []int64 `json:"subjectIds" form:"subjectIds" binding:"required"`
+	}
+	var validator Validator
+	err := c.ShouldBind(&validator)
+	if err != nil {
+		response.FailValidator(c, err)
+		return
+	}
+
+	removeErr := questionnaire_template.DeleteSubject(validator.ID, validator.SubjectIds)
+	if removeErr != nil {
+		response.Fail(c, removeErr)
+		return
+	}
+
+	response.Success(c, map[string]any{})
+}
+
+// 批量修改关联问题库的排序
+func UpdateQuestionnaireSubjectSort(c *gin.Context) {
+	type Validator struct {
+		ID         int64   `json:"id" form:"id" binding:"required"`
+		SubjectIds []int64 `json:"subjectIds" form:"subjectIds" binding:"required"`
+	}
+	var validator Validator
+	err := c.ShouldBind(&validator)
+	if err != nil {
+		response.FailValidator(c, err)
+		return
+	}
+
+	updateErr := questionnaire_template.UpdateSort(validator.ID, validator.SubjectIds)
+	if updateErr != nil {
+		response.Fail(c, updateErr)
+		return
+	}
+
+	response.Success(c, map[string]any{})
+}

+ 149 - 0
controller/survey/manage.go

@@ -0,0 +1,149 @@
+package survey
+
+import (
+	"surveyService/response"
+	"surveyService/service/survey"
+	"surveyService/validators"
+
+	"github.com/gin-gonic/gin"
+)
+
+// 创建问卷
+func UpdateOrCreate(c *gin.Context) {
+	var validator *validators.Survey
+	validateErr := c.ShouldBind(&validator)
+	if validateErr != nil {
+		response.FailValidator(c, validateErr)
+		return
+	}
+
+	err := survey.UpdateOrCreate(validator)
+	if err != nil {
+		response.Fail(c, err)
+		return
+	}
+
+	response.Success(c, map[string]any{})
+}
+
+// 获取问卷基本信息
+func Detail(c *gin.Context) {
+	type Validator struct {
+		ID string `json:"id" form:"id" binding:"required"`
+	}
+	var validator Validator
+	validateErr := c.ShouldBind(&validator)
+	if validateErr != nil {
+		response.FailValidator(c, validateErr)
+		return
+	}
+
+	surveyModel, err := survey.FindBySn(validator.ID)
+	if err != nil {
+		response.Fail(c, err)
+		return
+	}
+
+	response.Success(c, map[string]any{
+		"detail": survey.Format(surveyModel),
+	})
+}
+
+// 获取问卷列表
+func List(c *gin.Context) {
+	type Validator struct {
+		Key        string `json:"key" form:"key" binding:"omitempty"`
+		SurveyCode string `json:"surveyCode" form:"surveyCode" binding:"omitempty"`
+		Status     int    `json:"status" form:"status" binding:"omitempty,min=1,max=2"`
+	}
+	var validator Validator
+	validateErr := c.ShouldBind(&validator)
+	if validateErr != nil {
+		response.FailValidator(c, validateErr)
+		return
+	}
+
+	surveys := survey.List(validator.Key, validator.SurveyCode, validator.Status)
+
+	formatedSurveys := make([]*validators.Survey, 0)
+	for _, surveyModel := range surveys {
+		formatedSurveys = append(formatedSurveys, survey.Format(surveyModel))
+	}
+
+	response.Success(c, map[string]any{
+		"list": formatedSurveys,
+	})
+}
+
+// 分页获取问卷列表
+func Paginate(c *gin.Context) {
+	type Validator struct {
+		Page       int    `json:"page" form:"page" binding:"omitempty,min=1"`
+		PageSize   int    `json:"pageSize" form:"pageSize" binding:"omitempty,min=1"`
+		Key        string `json:"key" form:"key" binding:"omitempty"`
+		SurveyCode string `json:"surveyCode" form:"surveyCode" binding:"omitempty"`
+		Status     int    `json:"status" form:"status" binding:"omitempty,min=1,max=2"`
+	}
+	var validator Validator
+	validateErr := c.ShouldBind(&validator)
+	if validateErr != nil {
+		response.FailValidator(c, validateErr)
+		return
+	}
+
+	surveys, total := survey.Paginate(validator.Page, validator.PageSize, validator.Key, validator.SurveyCode, validator.Status)
+
+	formatedSurveys := make([]*validators.Survey, 0)
+	for _, surveyModel := range surveys {
+		formatedSurveys = append(formatedSurveys, survey.Format(surveyModel))
+	}
+
+	response.Success(c, map[string]any{
+		"list":  formatedSurveys,
+		"total": total,
+	})
+}
+
+// 修改备注
+func UpdateRemark(c *gin.Context) {
+	type Validator struct {
+		ID     string `json:"id" form:"id" binding:"required"`
+		Remark string `json:"remark" form:"remark" binding:"required"`
+	}
+	var validator Validator
+	validateErr := c.ShouldBind(&validator)
+	if validateErr != nil {
+		response.FailValidator(c, validateErr)
+		return
+	}
+
+	err := survey.UpdateRemark(validator.ID, validator.Remark)
+	if err != nil {
+		response.Fail(c, err)
+		return
+	}
+
+	response.Success(c, map[string]any{})
+}
+
+// 修改状态
+func UpdateStatus(c *gin.Context) {
+	type Validator struct {
+		ID     string `json:"id" form:"id" binding:"required"`
+		Status int    `json:"status" form:"status" binding:"required,min=1,max=2"`
+	}
+	var validator Validator
+	validateErr := c.ShouldBind(&validator)
+	if validateErr != nil {
+		response.FailValidator(c, validateErr)
+		return
+	}
+
+	err := survey.UpdateStatus(validator.ID, validator.Status)
+	if err != nil {
+		response.Fail(c, err)
+		return
+	}
+
+	response.Success(c, map[string]any{})
+}

+ 63 - 0
controller/survey_mechanism/external.go

@@ -0,0 +1,63 @@
+package survey_mechanism
+
+import (
+	"surveyService/response"
+	"surveyService/service/survey_mechanism"
+	"surveyService/util"
+	"surveyService/util/constants"
+	"surveyService/validators"
+
+	"github.com/gin-gonic/gin"
+	"gogs.uu.mdfitnesscao.com/hys/sdk"
+)
+
+// 获取问卷列表
+func List(c *gin.Context) {
+	// type Validator struct {
+	// 	Key    string `json:"key" form:"key" binding:"omitempty"`
+	// 	Status int    `json:"status" form:"status" binding:"omitempty,min=1,max=2"`
+	// }
+	// var validator Validator
+	// validateErr := c.ShouldBind(&validator)
+	// if validateErr != nil {
+	// 	response.FailValidator(c, validateErr)
+	// 	return
+	// }
+
+	mechanismInfo, _ := util.GetFromGinContext[*sdk.AuthMechanism](c, constants.MechanismOpenAPICacheKey)
+
+	surveyMechanisms := survey_mechanism.InitMechanism(mechanismInfo).List(0, "")
+	var list []*validators.SurveyMechanism = make([]*validators.SurveyMechanism, 0)
+	for _, surveyMechanism := range surveyMechanisms {
+		list = append(list, survey_mechanism.Format(surveyMechanism, nil, false))
+	}
+
+	response.Success(c, map[string]any{
+		"list": list,
+	})
+}
+
+// 通过某个问卷编号获取H5端问卷答题地址
+func GetLinkBySurveyId(c *gin.Context) {
+	type Validator struct {
+		Extra    string `json:"extra" form:"extra" binding:"required"`       // 自定义参数
+		SurveyId string `json:"surveyId" form:"surveyId" binding:"required"` // 问卷编号
+	}
+	var validator Validator
+	if err := c.ShouldBind(&validator); err != nil {
+		response.FailValidator(c, err)
+		return
+	}
+	mechanismInfo, _ := util.GetFromGinContext[*sdk.AuthMechanism](c, constants.MechanismOpenAPICacheKey)
+
+	link, expiredIn, linkErr := survey_mechanism.InitMechanism(mechanismInfo).GetSurveyVisitLink(validator.SurveyId, validator.Extra)
+	if linkErr != nil {
+		response.Fail(c, linkErr)
+		return
+	}
+
+	response.Success(c, map[string]any{
+		"link":          link,
+		"linkExpiredIn": expiredIn,
+	})
+}

+ 103 - 0
controller/survey_mechanism/manage.go

@@ -0,0 +1,103 @@
+package survey_mechanism
+
+import (
+	"surveyService/response"
+	"surveyService/service/survey_mechanism"
+
+	"github.com/gin-gonic/gin"
+)
+
+// 开始创建问卷授权
+func CreateForManage(c *gin.Context) {
+	type Validator struct {
+		SurveyId     string   `json:"surveyId" form:"surveyId" binding:"required"`
+		MechanismIds []string `json:"mechanismIds" form:"mechanismIds" binding:"required"`
+	}
+	var validator Validator
+	validateErr := c.ShouldBind(&validator)
+	if validateErr != nil {
+		response.FailValidator(c, validateErr)
+		return
+	}
+
+	err := survey_mechanism.Create(validator.SurveyId, validator.MechanismIds)
+	if err != nil {
+		response.Fail(c, err)
+		return
+	}
+
+	response.Success(c, map[string]any{})
+}
+
+// 获取某个问卷已被授权的机构列表
+func ListMechanismIdsForSurveyForManage(c *gin.Context) {
+	type Validator struct {
+		SurveyId string `json:"surveyId" form:"surveyId" binding:"required"`
+	}
+	var validator Validator
+	validateErr := c.ShouldBindQuery(&validator)
+	if validateErr != nil {
+		response.FailValidator(c, validateErr)
+		return
+	}
+
+	mechanismIds, err := survey_mechanism.ListMechanismIdsForSurvey(validator.SurveyId)
+	if err != nil {
+		response.Fail(c, err)
+		return
+	}
+
+	response.Success(c, map[string]any{
+		"list": mechanismIds,
+	})
+}
+
+// 获取授权列表
+func PaginateForManage(c *gin.Context) {
+	type Validator struct {
+		Page        int    `json:"page" form:"page" binding:"omitempty,min=1"`
+		PageSize    int    `json:"pageSize" form:"pageSize" binding:"omitempty,min=1"`
+		Key         string `json:"key" form:"key" binding:"omitempty"`
+		MechanismId string `json:"mechanismId" form:"mechanismId" binding:"omitempty"`
+		SurveyId    string `json:"surveyId" form:"surveyId" binding:"omitempty"`
+	}
+	var validator Validator
+	validateErr := c.ShouldBindQuery(&validator)
+	if validateErr != nil {
+		response.FailValidator(c, validateErr)
+		return
+	}
+
+	surveyMechanisms, total, err := survey_mechanism.Paginate(validator.Page, validator.PageSize, validator.Key, validator.MechanismId, validator.SurveyId)
+	if err != nil {
+		response.Fail(c, err)
+		return
+	}
+
+	response.Success(c, map[string]any{
+		"list":  survey_mechanism.ListFormat(surveyMechanisms),
+		"total": total,
+	})
+}
+
+// 修改授权状态
+func UpdateAuthorizeStatusForManage(c *gin.Context) {
+	type Validator struct {
+		ID     string `json:"id" form:"id" binding:"required"`
+		Status int    `json:"status" form:"status" binding:"required"`
+	}
+	var validator Validator
+	validateErr := c.ShouldBind(&validator)
+	if validateErr != nil {
+		response.FailValidator(c, validateErr)
+		return
+	}
+
+	err := survey_mechanism.UpdateAuthorizeStatus(validator.ID, validator.Status)
+	if err != nil {
+		response.Fail(c, err)
+		return
+	}
+
+	response.Success(c, map[string]any{})
+}

+ 136 - 0
controller/survey_mechanism/mechanism.go

@@ -0,0 +1,136 @@
+package survey_mechanism
+
+import (
+	"surveyService/model"
+	"surveyService/response"
+	"surveyService/service/survey_mechanism"
+	"surveyService/util"
+	"surveyService/util/constants"
+	"surveyService/validators"
+
+	"github.com/gin-gonic/gin"
+	"gogs.uu.mdfitnesscao.com/hys/sdk"
+)
+
+// 获取详情
+func Detail(c *gin.Context) {
+	type Validator struct {
+		ID string `json:"id" form:"id" binding:"required"`
+	}
+	var validator Validator
+	validateErr := c.ShouldBindQuery(&validator)
+	if validateErr != nil {
+		response.FailValidator(c, validateErr)
+		return
+	}
+
+	authMechanism, _ := util.GetFromGinContext[*sdk.AuthMechanism](c, constants.MechanismCacheKey)
+
+	surveyMechanism, err := survey_mechanism.InitMechanism(authMechanism).Detail(validator.ID)
+	if err != nil {
+		response.Fail(c, err)
+		return
+	}
+
+	response.Success(c, map[string]any{
+		"detail": survey_mechanism.ListFormat([]*model.SurveyMechanism{surveyMechanism})[0],
+	})
+}
+
+// 修改问卷信息
+func Update(c *gin.Context) {
+	var validator *validators.SurveyMechanism
+	validateErr := c.ShouldBind(&validator)
+	if validateErr != nil {
+		response.FailValidator(c, validateErr)
+		return
+	}
+
+	authMechanism, _ := util.GetFromGinContext[*sdk.AuthMechanism](c, constants.MechanismCacheKey)
+
+	err := survey_mechanism.InitMechanism(authMechanism).Update(validator)
+	if err != nil {
+		response.Fail(c, err)
+		return
+	}
+
+	response.Success(c, map[string]any{})
+}
+
+// 修改问卷状态
+func UpdateStatus(c *gin.Context) {
+	type Validator struct {
+		ID     string `json:"id" form:"id" binding:"required"`
+		Status int    `json:"status" form:"status" binding:"required,min=1,max=2"`
+	}
+	var validator Validator
+	validateErr := c.ShouldBind(&validator)
+	if validateErr != nil {
+		response.FailValidator(c, validateErr)
+		return
+	}
+
+	authMechanism, _ := util.GetFromGinContext[*sdk.AuthMechanism](c, constants.MechanismCacheKey)
+
+	err := survey_mechanism.InitMechanism(authMechanism).UpdateStatus(validator.ID, validator.Status)
+	if err != nil {
+		response.Fail(c, err)
+		return
+	}
+
+	response.Success(c, map[string]any{})
+}
+
+// 获取问卷列表
+func Paginate(c *gin.Context) {
+	type Validator struct {
+		Page     int    `json:"page" form:"page" binding:"omitempty,min=1"`
+		PageSize int    `json:"pageSize" form:"pageSize" binding:"omitempty,min=1"`
+		Key      string `json:"key" form:"key" binding:"omitempty"`
+		Status   int    `json:"status" form:"status" binding:"omitempty,min=1,max=2"`
+	}
+	var validator Validator
+	validateErr := c.ShouldBind(&validator)
+	if validateErr != nil {
+		response.FailValidator(c, validateErr)
+		return
+	}
+
+	authMechanism, _ := util.GetFromGinContext[*sdk.AuthMechanism](c, constants.MechanismCacheKey)
+
+	surveyMechanisms, total, err := survey_mechanism.InitMechanism(authMechanism).Paginate(validator.Page, validator.PageSize, validator.Status, validator.Key)
+	if err != nil {
+		response.Fail(c, err)
+		return
+	}
+
+	response.Success(c, map[string]any{
+		"list":  survey_mechanism.ListFormat(surveyMechanisms),
+		"total": total,
+	})
+}
+
+// 获取问卷的题目配置
+func DetailSurveyConfig(c *gin.Context) {
+	type Validator struct {
+		ID string `json:"id" form:"id" binding:"required"`
+	}
+	var validator Validator
+	validateErr := c.ShouldBindQuery(&validator)
+	if validateErr != nil {
+		response.FailValidator(c, validateErr)
+		return
+	}
+
+	authMechanism, _ := util.GetFromGinContext[*sdk.AuthMechanism](c, constants.MechanismCacheKey)
+
+	surveyConfig, err := survey_mechanism.InitMechanism(authMechanism).DetailSurveyConfig(validator.ID)
+	if err != nil {
+		response.Fail(c, err)
+		return
+	}
+
+	response.Success(c, map[string]any{
+		"detail": surveyConfig,
+	})
+}

+ 101 - 0
controller/survey_mechanism/member.go

@@ -0,0 +1,101 @@
+package survey_mechanism
+
+import (
+	"surveyService/response"
+	"surveyService/service/survey_mechanism"
+	"surveyService/util"
+	"surveyService/util/constants"
+	"surveyService/validators"
+
+	"github.com/gin-gonic/gin"
+	"gogs.uu.mdfitnesscao.com/hys/sdk"
+)
+
+// 获取问卷基本信息
+func DetailForMember(c *gin.Context) {
+	type Validator struct {
+		ID string `json:"id" form:"id" binding:"required"`
+	}
+	var validator Validator
+	if validateErr := c.ShouldBindQuery(&validator); validateErr != nil {
+		response.FailValidator(c, validateErr)
+		return
+	}
+	mechanismId, _ := util.GetFromGinContext[string](c, constants.MemberMechanismIDKey)
+	member, _ := util.GetFromGinContext[*sdk.AuthMember](c, constants.MemberCacheKey)
+	surveyTokenData, _ := util.GetFromGinContext[*validators.SurveyToken](c, constants.SurveyTokenDataCacheKey)
+	surveyId := validator.ID
+	if surveyTokenData != nil {
+		surveyId = surveyTokenData.SurveyId
+	}
+
+	survey, findErr := survey_mechanism.InitMember(member, mechanismId).Detail(surveyId)
+	if findErr != nil {
+		response.Fail(c, findErr)
+		return
+	}
+
+	response.Success(c, map[string]any{
+		"detail": survey_mechanism.InitMember(member, mechanismId).FormatSurvey(survey),
+	})
+}
+
+// 获取问卷题目列表
+func DetailQuestionsForMember(c *gin.Context) {
+	type Validator struct {
+		ID string `json:"id" form:"id" binding:"required"`
+	}
+	var validator Validator
+	if validateErr := c.ShouldBindQuery(&validator); validateErr != nil {
+		response.FailValidator(c, validateErr)
+		return
+	}
+	mechanismId, _ := util.GetFromGinContext[string](c, constants.MemberMechanismIDKey)
+	member, _ := util.GetFromGinContext[*sdk.AuthMember](c, constants.MemberCacheKey)
+
+	surveyTokenData, _ := util.GetFromGinContext[*validators.SurveyToken](c, constants.SurveyTokenDataCacheKey)
+	surveyId := validator.ID
+	if surveyTokenData != nil {
+		surveyId = surveyTokenData.SurveyId
+	}
+
+	surveyQuestions, err := survey_mechanism.InitMember(member, mechanismId).DetailSurveyQuestions(surveyId)
+	if err != nil {
+		response.Fail(c, err)
+		return
+	}
+
+	response.Success(c, map[string]any{
+		"questions": surveyQuestions,
+	})
+}
+
+// 获取问卷列表
+func ListForMember(c *gin.Context) {
+
+	type Validator struct {
+		Type int64 `json:"type" form:"type" binding:"omitempty"`
+	}
+	var validator Validator
+	if validateErr := c.ShouldBindQuery(&validator); validateErr != nil {
+		response.FailValidator(c, validateErr)
+		return
+	}
+
+	mechanismId, _ := util.GetFromGinContext[string](c, constants.MemberMechanismIDKey)
+	member, _ := util.GetFromGinContext[*sdk.AuthMember](c, constants.MemberCacheKey)
+	// if validator.Type == 0 {
+	// 	validator.Type = 1
+	// }
+	surveyMechanismList := survey_mechanism.InitMember(member, mechanismId).List(
+		validator.Type,
+	)
+
+	var list []*validators.SurveyMechanism = make([]*validators.SurveyMechanism, 0)
+	for _, surveyMechanism := range surveyMechanismList {
+		list = append(list, survey_mechanism.InitMember(member, mechanismId).FormatSurvey(surveyMechanism))
+	}
+	response.Success(c, map[string]any{
+		"list": list,
+	})
+}

+ 37 - 0
controller/survey_result/external.go

@@ -0,0 +1,37 @@
+package survey_result
+
+import (
+	"surveyService/response"
+	"surveyService/service/survey_result"
+	"surveyService/util"
+	"surveyService/util/constants"
+
+	"github.com/gin-gonic/gin"
+	"gogs.uu.mdfitnesscao.com/hys/sdk"
+)
+
+// 通过某个问卷结果编号获取H5端结果页地址
+func GetLinkBySurveyResultId(c *gin.Context) {
+	type Validator struct {
+		Extra string `json:"extra" form:"extra" binding:"required"` // 自定义参数
+	}
+	var validator Validator
+	if err := c.ShouldBind(&validator); err != nil {
+		response.FailValidator(c, err)
+		return
+	}
+	mechanismInfo, _ := util.GetFromGinContext[*sdk.AuthMechanism](c, constants.MechanismOpenAPICacheKey)
+
+	link, expiredIn, surveyResult, linkErr := survey_result.InitMechanism(mechanismInfo).GetSurveyResultVisitLink(validator.Extra)
+	if linkErr != nil {
+		response.Fail(c, linkErr)
+		return
+	}
+
+	response.Success(c, map[string]any{
+		"link":          link,
+		"linkExpiredIn": expiredIn,
+		"status":        surveyResult.Status,
+	})
+
+}

+ 79 - 0
controller/survey_result/manage.go

@@ -0,0 +1,79 @@
+package survey_result
+
+import (
+	"surveyService/response"
+	"surveyService/service/survey_result"
+	"surveyService/validators"
+
+	"github.com/gin-gonic/gin"
+)
+
+// 获取详情
+func DetailForManage(c *gin.Context) {
+	type Validator struct {
+		ID string `json:"id" form:"id" binding:"required"`
+	}
+	var validator Validator
+	validateErr := c.ShouldBindQuery(&validator)
+	if validateErr != nil {
+		response.FailValidator(c, validateErr)
+		return
+	}
+
+	surveyResult, err := survey_result.InitMechanism(nil).Detail(validator.ID)
+	if err != nil {
+		response.Fail(c, err)
+		return
+	}
+
+	response.Success(c, map[string]any{
+		"detail": survey_result.Format(surveyResult, true),
+	})
+}
+
+// 获取列表
+func PaginateForManage(c *gin.Context) {
+	var validator validators.SurveyResultPaginate
+	validateErr := c.ShouldBindQuery(&validator)
+	if validateErr != nil {
+		response.FailValidator(c, validateErr)
+		return
+	}
+
+	surveyResultList, total, err := survey_result.InitMechanism(nil).Paginate(validator)
+	if err != nil {
+		response.Fail(c, err)
+		return
+	}
+
+	var list = make([]*validators.SurveyResult, 0)
+	for _, surveyResult := range surveyResultList {
+		list = append(list, survey_result.Format(surveyResult, false))
+	}
+
+	response.Success(c, map[string]any{
+		"list":  list,
+		"total": total,
+	})
+}
+
+// 重新执行一次
+func RunForManage(c *gin.Context) {
+	type Validator struct {
+		ID string `json:"id" form:"id" binding:"required"`
+	}
+	var validator Validator
+	validateErr := c.ShouldBind(&validator)
+	if validateErr != nil {
+		response.FailValidator(c, validateErr)
+		return
+	}
+
+	err := survey_result.InitMechanism(nil).Run(validator.ID)
+	if err != nil {
+		response.Fail(c, err)
+		return
+	}
+
+	response.Success(c, map[string]any{})
+}

+ 65 - 0
controller/survey_result/mechanism.go

@@ -0,0 +1,65 @@
+package survey_result
+
+import (
+	"surveyService/response"
+	"surveyService/service/survey_result"
+	"surveyService/util"
+	"surveyService/util/constants"
+	"surveyService/validators"
+
+	"github.com/gin-gonic/gin"
+	"gogs.uu.mdfitnesscao.com/hys/sdk"
+)
+
+// 获取详情
+func Detail(c *gin.Context) {
+	type Validator struct {
+		ID string `json:"id" form:"id" binding:"required"`
+	}
+	var validator Validator
+	validateErr := c.ShouldBindQuery(&validator)
+	if validateErr != nil {
+		response.FailValidator(c, validateErr)
+		return
+	}
+
+	authMechanism, _ := util.GetFromGinContext[*sdk.AuthMechanism](c, constants.MechanismCacheKey)
+
+	surveyResult, err := survey_result.InitMechanism(authMechanism).Detail(validator.ID)
+	if err != nil {
+		response.Fail(c, err)
+		return
+	}
+
+	response.Success(c, map[string]any{
+		"detail": survey_result.Format(surveyResult, true),
+	})
+}
+
+// 获取列表
+func Paginate(c *gin.Context) {
+	var validator validators.SurveyResultPaginate
+	validateErr := c.ShouldBindQuery(&validator)
+	if validateErr != nil {
+		response.FailValidator(c, validateErr)
+		return
+	}
+
+	authMechanism, _ := util.GetFromGinContext[*sdk.AuthMechanism](c, constants.MechanismCacheKey)
+
+	surveyResultList, total, err := survey_result.InitMechanism(authMechanism).Paginate(validator)
+	if err != nil {
+		response.Fail(c, err)
+		return
+	}
+
+	var list = make([]*validators.SurveyResult, 0)
+	for _, surveyResult := range surveyResultList {
+		list = append(list, survey_result.Format(surveyResult, false))
+	}
+
+	response.Success(c, map[string]any{
+		"list":  list,
+		"total": total,
+	})
+}

+ 222 - 0
controller/survey_result/member.go

@@ -0,0 +1,222 @@
+package survey_result
+
+import (
+	"surveyService/response"
+	"surveyService/service/survey_mechanism"
+	"surveyService/service/survey_result"
+	"surveyService/service/survey_token"
+	"surveyService/util"
+	"surveyService/util/constants"
+	"surveyService/validators"
+
+	"github.com/gin-gonic/gin"
+	"gogs.uu.mdfitnesscao.com/hys/sdk"
+)
+
+// 创建问卷答题结果
+func CreateSurveyResultForMember(c *gin.Context) {
+	type Validator struct {
+		ID string `json:"id" form:"id" binding:"required"`
+	}
+	var validator Validator
+	if validateErr := c.ShouldBind(&validator); validateErr != nil {
+		response.FailValidator(c, validateErr)
+		return
+	}
+	mechanismId, _ := util.GetFromGinContext[string](c, constants.MemberMechanismIDKey)
+	member, _ := util.GetFromGinContext[*sdk.AuthMember](c, constants.MemberCacheKey)
+
+	surveyToken, _ := util.GetFromGinContext[string](c, constants.SurveyTokenCacheKey)
+	surveyTokenData, _ := util.GetFromGinContext[*validators.SurveyToken](c, constants.SurveyTokenDataCacheKey)
+	surveyId := validator.ID
+	extra := ""
+	if surveyTokenData != nil {
+		surveyId = surveyTokenData.SurveyId
+		extra = surveyTokenData.Extra
+	}
+
+	surveyResult, err := survey_result.InitMember(member, mechanismId).Create(surveyId, extra)
+	if err != nil {
+		response.Fail(c, err)
+		return
+	}
+
+	surveyQuestions, err := survey_mechanism.InitMember(member, mechanismId).DetailSurveyQuestions(surveyId)
+	if err != nil {
+		response.Fail(c, err)
+		return
+	}
+	if surveyTokenData != nil {
+		// 将结果编号更新到Token里
+		survey_token.RenewSurveyToken(surveyToken, &validators.SurveyToken{
+			SurveyId:       surveyTokenData.SurveyId,
+			SurveyResultId: surveyResult.SN,
+			Extra:          surveyTokenData.Extra,
+			MechanismId:    surveyTokenData.MechanismId,
+		}, 7200)
+	}
+
+	response.Success(c, map[string]any{
+		"detail":    survey_result.Format(surveyResult, true),
+		"questions": surveyQuestions,
+	})
+}
+
+// 提交问卷答题结果
+func SubmitSurveyResultForMember(c *gin.Context) {
+	type Validator struct {
+		ID     string         `json:"id" form:"id" binding:"required"`
+		Answer map[string]any `json:"answer" form:"answer" binding:"required"`
+	}
+	var validator Validator
+	if validateErr := c.ShouldBind(&validator); validateErr != nil {
+		response.FailValidator(c, validateErr)
+		return
+	}
+	mechanismId, _ := util.GetFromGinContext[string](c, constants.MemberMechanismIDKey)
+	member, _ := util.GetFromGinContext[*sdk.AuthMember](c, constants.MemberCacheKey)
+	surveyTokenData, _ := util.GetFromGinContext[*validators.SurveyToken](c, constants.SurveyTokenDataCacheKey)
+	surveyResultId := validator.ID
+	if surveyTokenData != nil {
+		surveyResultId = surveyTokenData.SurveyResultId
+	}
+
+	err := survey_result.InitMember(member, mechanismId).Submit(surveyResultId, validator.Answer)
+	if err != nil {
+		response.Fail(c, err)
+		return
+	}
+
+	response.Success(c, map[string]any{})
+}
+
+// 创建并提交问卷答题结果
+func CreateAndSubmitSurveyResultForMember(c *gin.Context) {
+	type Validator struct {
+		ID        string         `json:"id" form:"id" binding:"required"`
+		Answer    map[string]any `json:"answer" form:"answer" binding:"required"`
+		StartTime string         `json:"startTime" form:"startTime" binding:"required"`
+	}
+	var validator Validator
+	if validateErr := c.ShouldBind(&validator); validateErr != nil {
+		response.FailValidator(c, validateErr)
+		return
+	}
+	mechanismId, _ := util.GetFromGinContext[string](c, constants.MemberMechanismIDKey)
+	member, _ := util.GetFromGinContext[*sdk.AuthMember](c, constants.MemberCacheKey)
+
+	surveyToken, _ := util.GetFromGinContext[string](c, constants.SurveyTokenCacheKey)
+	surveyTokenData, _ := util.GetFromGinContext[*validators.SurveyToken](c, constants.SurveyTokenDataCacheKey)
+	surveyId := validator.ID
+	extra := ""
+	if surveyTokenData != nil {
+		surveyId = surveyTokenData.SurveyId
+		extra = surveyTokenData.Extra
+	}
+
+	surveyResult, err := survey_result.InitMember(member, mechanismId).CreateAndSubmit(surveyId, validator.Answer, validator.StartTime, extra)
+	if err != nil {
+		response.Fail(c, err)
+		return
+	}
+
+	if surveyTokenData != nil {
+		// 将结果编号更新到Token里
+		survey_token.RenewSurveyToken(surveyToken, &validators.SurveyToken{
+			SurveyId:       surveyTokenData.SurveyId,
+			SurveyResultId: surveyResult.SN,
+			Extra:          surveyTokenData.Extra,
+			MechanismId:    surveyTokenData.MechanismId,
+		}, 7200)
+	}
+
+	response.Success(c, map[string]any{
+		"id": surveyResult.SN,
+	})
+}
+
+// 获取问卷答题结果
+func DetailSurveyResultForMember(c *gin.Context) {
+	type Validator struct {
+		ID string `json:"id" form:"id" binding:"required"`
+	}
+	var validator Validator
+	if validateErr := c.ShouldBindQuery(&validator); validateErr != nil {
+		response.FailValidator(c, validateErr)
+		return
+	}
+	mechanismId, _ := util.GetFromGinContext[string](c, constants.MemberMechanismIDKey)
+	member, _ := util.GetFromGinContext[*sdk.AuthMember](c, constants.MemberCacheKey)
+	surveyTokenData, _ := util.GetFromGinContext[*validators.SurveyToken](c, constants.SurveyTokenDataCacheKey)
+	surveyResultId := validator.ID
+	if surveyTokenData != nil {
+		surveyResultId = surveyTokenData.SurveyResultId
+	}
+
+	surveyResult, err := survey_result.InitMember(member, mechanismId).Detail(surveyResultId)
+	if err != nil {
+		response.Fail(c, err)
+		return
+	}
+
+	response.Success(c, map[string]any{
+		"detail": survey_result.Format(surveyResult, true),
+	})
+}
+
+// 获取问卷答题结果
+func DetailLastSurveyResultForMember(c *gin.Context) {
+	type Validator struct {
+		SurveyId         int64 `json:"surveyId" form:"surveyId" binding:"omitempty"`
+		IsPhysicalSurvey int64 `json:"isPhysicalSurvey" form:"isPhysicalSurvey" binding:"omitempty"`
+	}
+	var validator Validator
+	if validateErr := c.ShouldBindQuery(&validator); validateErr != nil {
+		response.FailValidator(c, validateErr)
+		return
+	}
+	if validator.IsPhysicalSurvey != 1 && validator.SurveyId == 0 {
+		response.Fail(c, &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "问卷编号是必填的",
+		})
+		return
+	}
+	mechanismId, _ := util.GetFromGinContext[string](c, constants.MemberMechanismIDKey)
+	member, _ := util.GetFromGinContext[*sdk.AuthMember](c, constants.MemberCacheKey)
+
+	surveyResult, err := survey_result.InitMember(member, mechanismId).DetailLastSurvey(validator.SurveyId, validator.IsPhysicalSurvey == 1)
+	if err != nil {
+		response.Fail(c, err)
+		return
+	}
+
+	response.Success(c, map[string]any{
+		"detail": survey_result.Format(surveyResult, true),
+	})
+}
+
+// 获取问卷结果列表
+func PaginateForMember(c *gin.Context) {
+	type Validator struct {
+		Page     int `json:"page" form:"page" binding:"omitempty,min=1"`
+		PageSize int `json:"pageSize" form:"pageSize" binding:"omitempty,min=1"`
+	}
+	var v Validator
+	if validateErr := c.ShouldBindQuery(&v); validateErr != nil {
+		response.FailValidator(c, validateErr)
+		return
+	}
+	mechanismId, _ := util.GetFromGinContext[string](c, constants.MemberMechanismIDKey)
+	member, _ := util.GetFromGinContext[*sdk.AuthMember](c, constants.MemberCacheKey)
+	surveyResults, total := survey_result.InitMember(member, mechanismId).Paginate(v.Page, v.PageSize)
+	var list []*validators.SurveyResult = make([]*validators.SurveyResult, 0)
+	for _, surveyResult := range surveyResults {
+		list = append(list, survey_result.Format(surveyResult, false))
+	}
+	response.Success(c, map[string]any{
+		"list":  list,
+		"total": total,
+	})
+
+}

+ 72 - 0
deploy.sh

@@ -0,0 +1,72 @@
+APP_NAME="survey-service"
+
+SERVER_IP="192.168.199.146"
+SERVER_USER="root"
+SERVER_SSH_PORT="22"
+SERVER_DEPLOY_DIR="/docker/services/$APP_NAME"
+SERVER_DEPLOY_APP="$SERVER_DEPLOY_DIR/app"
+SERVER_DEPLOY_ENV="$SERVER_DEPLOY_DIR/.env"
+SERVER_DEPLOY_DOCKER_FILE="$SERVER_DEPLOY_DIR/Dockerfile"
+SERVER_DOCKER_COMPOSE_DIR="/docker"
+
+OUTPUT_APP="./app"
+OUTPUT_ENV="./.env"
+OUTPUT_DOCKERFILE="./Dockerfile"
+
+# 本地打包
+bash ./build.sh
+
+# 检查远程目录是否存在
+ssh -p $SERVER_SSH_PORT $SERVER_USER@$SERVER_IP \
+    "
+        if [ ! -d ${SERVER_DEPLOY_DIR} ]; then
+            sudo mkdir -p ${SERVER_DEPLOY_DIR};
+        fi
+    "
+
+# 删除app文件
+ssh -p $SERVER_SSH_PORT $SERVER_USER@$SERVER_IP \
+    "
+        sudo rm -rf $SERVER_DEPLOY_APP;
+    "
+
+# 将应用文件上传到服务器(使用rsync+sudo)
+rsync -avz -e "ssh -p $SERVER_SSH_PORT" \
+    $OUTPUT_APP \
+    $SERVER_USER@$SERVER_IP:$SERVER_DEPLOY_APP \
+    --rsync-path="sudo rsync"
+
+# 将环境文件上传到服务器
+rsync -avz -e "ssh -p $SERVER_SSH_PORT" \
+    $OUTPUT_ENV \
+    $SERVER_USER@$SERVER_IP:$SERVER_DEPLOY_ENV \
+    --rsync-path="sudo rsync"
+
+# 将Dockerfile上传到服务器
+rsync -avz -e "ssh -p $SERVER_SSH_PORT" \
+    $OUTPUT_DOCKERFILE \
+    $SERVER_USER@$SERVER_IP:$SERVER_DEPLOY_DOCKER_FILE \
+    --rsync-path="sudo rsync"
+
+# # 将文件上传到服务器
+# scp -r -P $SERVER_SSH_PORT \
+#     $OUTPUT_APP \
+#     $SERVER_USER@$SERVER_IP:$SERVER_DEPLOY_APP
+
+# scp -r -P $SERVER_SSH_PORT \
+#     $OUTPUT_ENV \
+#     $SERVER_USER@$SERVER_IP:$SERVER_DEPLOY_ENV
+
+# scp -r -P $SERVER_SSH_PORT \
+#     $OUTPUT_DOCKERFILE \
+#     $SERVER_USER@$SERVER_IP:$SERVER_DEPLOY_DOCKER_FILE
+
+# 构建Docker等部署
+ssh -p $SERVER_SSH_PORT $SERVER_USER@$SERVER_IP \
+    "
+        cd ${SERVER_DOCKER_COMPOSE_DIR};
+        sudo docker build -t $APP_NAME --no-cache ${SERVER_DEPLOY_DIR}
+        sudo docker compose stop $APP_NAME
+        sudo docker compose rm -f $APP_NAME
+        sudo docker compose up -d $APP_NAME
+    "

+ 56 - 0
middleware/authorize.go

@@ -0,0 +1,56 @@
+package middleware
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"os"
+	"surveyService/cache"
+	"surveyService/response"
+	"surveyService/util/constants"
+
+	"github.com/gin-gonic/gin"
+	"github.com/go-redis/redis/v8"
+	"gogs.uu.mdfitnesscao.com/hys/sdk"
+)
+
+func Authorize() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		token := c.Request.Header.Get("token")
+		if token == "" {
+			response.Fail(c, response.ErrAuthorizationExpired)
+			return
+		}
+		var currentUser *sdk.AuthUser
+		var err *response.ErrCode
+		// 拿到用户系统的资料
+		currentUser, err = getTokenInfo(token)
+		if err != nil {
+			response.Fail(c, err)
+			return
+		}
+		c.Set(constants.UserCacheKey, currentUser)
+		c.Next()
+	}
+}
+
+// 根据Token获取用户信息
+func getTokenInfo(token string) (*sdk.AuthUser, *response.ErrCode) {
+	// 检查token是否存在
+	authTokenPrefix := os.Getenv("AUTH_TOKEN_PREFIX")
+	cacheKey := fmt.Sprintf("%s:Authorize:Token:%s", authTokenPrefix, token)
+	userInfoJson, err := cache.Instance().Get(cacheKey)
+	if err != nil {
+		if errors.Is(err, redis.Nil) {
+			return nil, response.ErrAuthorizationExpired
+		}
+		return nil, response.Err
+	}
+
+	var currentUser sdk.AuthUser
+	err = json.Unmarshal([]byte(userInfoJson), &currentUser)
+	if err != nil {
+		return nil, response.Err
+	}
+	return &currentUser, nil
+}

+ 56 - 0
middleware/authorize_mechanism.go

@@ -0,0 +1,56 @@
+package middleware
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"os"
+	"surveyService/cache"
+	"surveyService/response"
+	"surveyService/util/constants"
+
+	"github.com/gin-gonic/gin"
+	"github.com/go-redis/redis/v8"
+	"gogs.uu.mdfitnesscao.com/hys/sdk"
+)
+
+func AuthorizeMechanism() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		token := c.Request.Header.Get("token")
+		if token == "" {
+			response.Fail(c, response.ErrAuthorizationExpired)
+			return
+		}
+		var currentUser *sdk.AuthMechanism
+		var err *response.ErrCode
+		// 拿到用户系统的资料
+		currentUser, err = getMechanismTokenInfo(token)
+		if err != nil {
+			response.Fail(c, err)
+			return
+		}
+		c.Set(constants.MechanismCacheKey, currentUser)
+		c.Next()
+	}
+}
+
+// 根据Token获取用户信息
+func getMechanismTokenInfo(token string) (*sdk.AuthMechanism, *response.ErrCode) {
+	// 检查token是否存在
+	authTokenPrefix := os.Getenv("MECHANISM_AUTH_TOKEN_PREFIX")
+	cacheKey := fmt.Sprintf("%s:Authorize:Token:%s", authTokenPrefix, token)
+	userInfoJson, err := cache.Instance().Get(cacheKey)
+	if err != nil {
+		if errors.Is(err, redis.Nil) {
+			return nil, response.ErrAuthorizationExpired
+		}
+		return nil, response.Err
+	}
+
+	var currentUser sdk.AuthMechanism
+	err = json.Unmarshal([]byte(userInfoJson), &currentUser)
+	if err != nil {
+		return nil, response.Err
+	}
+	return &currentUser, nil
+}

+ 106 - 0
middleware/authorize_mechanism_by_appkey.go

@@ -0,0 +1,106 @@
+package middleware
+
+import (
+	"fmt"
+	"net/url"
+	"sort"
+	"strings"
+	"surveyService/response"
+	"surveyService/util"
+	"surveyService/util/constants"
+
+	"github.com/gin-gonic/gin"
+	"gogs.uu.mdfitnesscao.com/hys/sdk"
+	"gogs.uu.mdfitnesscao.com/hys/sdk/mechanism"
+)
+
+func AuthorizeMechanismByAppKey() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		appKey := c.Request.Header.Get("appkey")
+		if appKey == "" {
+			response.Fail(c, response.ErrInvalidToken)
+			return
+		}
+		sign := c.Request.Header.Get("sign")
+		if sign == "" {
+			response.Fail(c, response.ErrSign)
+			return
+		}
+		var currentUser *sdk.AuthMechanism
+		var err *response.ErrCode
+		// 拿到用户系统的资料
+		detailResponse, findErr := mechanism.DetailByAppKey(appKey)
+		if findErr != nil {
+			response.Fail(c, &response.ErrCode{
+				Code: response.INVALID_TOKEN,
+				Msg:  findErr.Msg,
+			})
+			return
+		}
+
+		currentUser = &sdk.AuthMechanism{
+			ID:             detailResponse.Data.Detail.ID,
+			Nickname:       detailResponse.Data.Detail.Nickname,
+			Account:        detailResponse.Data.Detail.Account,
+			LoginAt:        0,
+			IsMechanism:    true,
+			FullPermission: false,
+			Permissions:    detailResponse.Data.Detail.Permissions,
+			Mechanism: &sdk.AuthBaseMechanism{
+				ID:              detailResponse.Data.Detail.ID,
+				Nickname:        detailResponse.Data.Detail.Nickname,
+				Account:         detailResponse.Data.Detail.Account,
+				RawId:           detailResponse.Data.Detail.RawId,
+				AppKey:          detailResponse.Data.Detail.AppKey,
+				AppSecret:       detailResponse.Data.Detail.AppSecret,
+				IpWhiteList:     detailResponse.Data.Detail.IpWhiteList,
+				CallbackUrlList: detailResponse.Data.Detail.CallbackUrlList,
+			},
+		}
+		if err != nil {
+			response.Fail(c, err)
+			return
+		}
+
+		// 校验签名
+		requestData := util.GetRequestJsonData(c)
+		makedSign := makeSign(requestData, currentUser.Mechanism.AppSecret)
+		// fmt.Println(makedSign, sign)
+		if makedSign != sign {
+			response.Fail(c, response.ErrSign)
+			return
+		}
+		c.Set(constants.MechanismOpenAPICacheKey, currentUser)
+		c.Next()
+	}
+}
+
+func makeSign(requestData map[string]any, appSecret string) string {
+	// 将map的key存储到一个切片中
+	keys := make([]string, 0, len(requestData))
+	for k := range requestData {
+		keys = append(keys, k)
+	}
+	// 对切片进行排序
+	sort.Strings(keys)
+
+	// 将请求参数按照key的升序排列
+	var requestDataArray []string = make([]string, 0)
+	// 遍历排序后的切片,并使用map的key来访问map中的值
+	for _, k := range keys {
+		v := requestData[k]
+		requestDataArray = append(requestDataArray, k+"="+url.QueryEscape(fmt.Sprint(v)))
+	}
+	// 拼接成key1=value1&key2=value2&key3=value3的形式
+	requestDataString := strings.Join(requestDataArray, "&")
+	// fmt.Println("拼装后字段", requestDataString)
+
+	// 将appSecret拼接到最后
+	signString := requestDataString + appSecret
+	// fmt.Println("待签名字段", signString)
+
+	// 计算md5
+	sign := strings.ToUpper(util.Md5(signString))
+	// fmt.Println("签名: ", sign)
+	return sign
+}

+ 59 - 0
middleware/authorize_member.go

@@ -0,0 +1,59 @@
+package middleware
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"surveyService/cache"
+	"surveyService/response"
+	"surveyService/util/constants"
+
+	"github.com/gin-gonic/gin"
+	"github.com/go-redis/redis/v8"
+	"gogs.uu.mdfitnesscao.com/hys/sdk"
+)
+
+func AuthorizeMember() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		// 如果有第三方的拦截逻辑,跳过该部分
+		surveyToken := c.Request.Header.Get("surveyToken")
+		if surveyToken == "" {
+			token := c.Request.Header.Get("token")
+			if token == "" {
+				response.Fail(c, response.ErrAuthorizationExpired)
+				return
+			}
+			// 拿到用户系统的资料
+			currentUser, err := getMemberTokenInfo[*sdk.AuthMember](token)
+			if err != nil {
+				response.Fail(c, err)
+				return
+			}
+			c.Set(constants.MemberCacheKey, currentUser)
+		}
+		c.Next()
+	}
+}
+
+// 根据Token获取用户信息
+func getMemberTokenInfo[T any](token string) (T, *response.ErrCode) {
+	// 检查token是否存在
+	var currentUser T
+	cacheKey := fmt.Sprintf("archivesService:Authorize:Token:%s", token)
+	userInfoJson, err := cache.Instance().Get(cacheKey)
+	if err != nil {
+		if errors.Is(err, redis.Nil) {
+			return currentUser, &response.ErrCode{
+				Code: response.ErrAuthorizationExpired.Code,
+				Msg:  "登录已过期,请重新登录",
+			}
+		}
+		return currentUser, response.Err
+	}
+
+	err = json.Unmarshal([]byte(userInfoJson), &currentUser)
+	if err != nil {
+		return currentUser, response.Err
+	}
+	return currentUser, nil
+}

+ 32 - 0
middleware/authorize_member_omitempty.go

@@ -0,0 +1,32 @@
+package middleware
+
+import (
+	"surveyService/response"
+	"surveyService/util/constants"
+
+	"github.com/gin-gonic/gin"
+	"gogs.uu.mdfitnesscao.com/hys/sdk"
+)
+
+// 拦截用户端的机构ID
+func AuthorizeMemberOmitempty() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		mechanismId := c.Request.Header.Get("mechanismId")
+		c.Set(constants.MemberMechanismIDKey, mechanismId)
+		// 如果有第三方的拦截逻辑,跳过该部分
+		surveyToken := c.Request.Header.Get("surveyToken")
+		if surveyToken == "" {
+			token := c.Request.Header.Get("token")
+			if token != "" {
+				// 拿到用户系统的资料
+				currentUser, err := getMemberTokenInfo[*sdk.AuthMember](token)
+				if err != nil {
+					response.Fail(c, err)
+					return
+				}
+				c.Set(constants.MemberCacheKey, currentUser)
+			}
+		}
+		c.Next()
+	}
+}

+ 33 - 0
middleware/authorize_survey_token.go

@@ -0,0 +1,33 @@
+package middleware
+
+import (
+	"surveyService/response"
+	"surveyService/service/survey_token"
+	"surveyService/util/constants"
+	"surveyService/validators"
+
+	"github.com/gin-gonic/gin"
+)
+
+func AuthorizeSurveyToken() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		token := c.Request.Header.Get("surveyToken")
+		if token == "" {
+			c.Set(constants.SurveyTokenCacheKey, "")
+			c.Set(constants.SurveyTokenDataCacheKey, nil)
+			c.Next()
+			return
+		}
+		var currentUser *validators.SurveyToken
+		var err *response.ErrCode
+		// 拿到用户系统的资料
+		currentUser, err = survey_token.GetSurveyTokenData(token)
+		if err != nil {
+			response.Fail(c, err)
+			return
+		}
+		c.Set(constants.SurveyTokenCacheKey, token)
+		c.Set(constants.SurveyTokenDataCacheKey, currentUser)
+		c.Next()
+	}
+}

+ 23 - 0
middleware/cors.go

@@ -0,0 +1,23 @@
+package middleware
+
+import (
+	"net/http"
+
+	"github.com/gin-gonic/gin"
+)
+
+func Cors() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		c.Header("Access-Control-Allow-Origin", "*")
+		c.Header("Access-Control-Allow-Headers", c.Request.Header.Get("Access-Control-Request-Headers"))
+		c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
+		c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")
+		c.Header("Access-Control-Allow-Credentials", "false")
+
+		if c.Request.Method == "OPTIONS" {
+			c.AbortWithStatus(http.StatusOK)
+			return
+		}
+		c.Next()
+	}
+}

+ 23 - 0
middleware/member_mechanism.go

@@ -0,0 +1,23 @@
+package middleware
+
+import (
+	"surveyService/response"
+	"surveyService/util/constants"
+
+	"github.com/gin-gonic/gin"
+)
+
+func MemberMechanism() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		mechanismId := c.Request.Header.Get("mechanismId")
+		if mechanismId == "" {
+			response.Fail(c, &response.ErrCode{
+				Code: response.MECHANISM_LOST,
+				Msg:  "无效机构来源",
+			})
+			return
+		}
+		c.Set(constants.MemberMechanismIDKey, mechanismId)
+		c.Next()
+	}
+}

+ 28 - 0
middleware/permission_check.go

@@ -0,0 +1,28 @@
+package middleware
+
+import (
+	"fmt"
+	"surveyService/response"
+	"surveyService/util"
+	"surveyService/util/constants"
+
+	"github.com/gin-gonic/gin"
+	"gogs.uu.mdfitnesscao.com/hys/sdk"
+	sdk_constants "gogs.uu.mdfitnesscao.com/hys/sdk/constants"
+)
+
+func PermissionCheck(routePermission string) gin.HandlerFunc {
+	return func(c *gin.Context) {
+		currentUser, _ := util.GetFromGinContext[*sdk.AuthUser](c, constants.UserCacheKey)
+		if !currentUser.FullPermission {
+			if !util.InArrayString(routePermission, currentUser.Permissions) {
+				response.Fail(c, &response.ErrCode{
+					Code: response.PERMISSION_NOT_ALLOWED,
+					Msg:  fmt.Sprintf("您没有权限进行该操作,缺少权限【%s】", sdk_constants.UserPermissionNames[routePermission].Name),
+				})
+				return
+			}
+		}
+		c.Next()
+	}
+}

+ 28 - 0
middleware/permission_check_staff.go

@@ -0,0 +1,28 @@
+package middleware
+
+import (
+	"fmt"
+	"surveyService/response"
+	"surveyService/util"
+	"surveyService/util/constants"
+
+	"github.com/gin-gonic/gin"
+	"gogs.uu.mdfitnesscao.com/hys/sdk"
+	sdk_constants "gogs.uu.mdfitnesscao.com/hys/sdk/constants"
+)
+
+func PermissionCheckStaff(routePermission string) gin.HandlerFunc {
+	return func(c *gin.Context) {
+		currentUser, _ := util.GetFromGinContext[*sdk.AuthMechanism](c, constants.MechanismCacheKey)
+		if !currentUser.FullPermission {
+			if !util.InArrayString(routePermission, currentUser.Permissions) {
+				response.Fail(c, &response.ErrCode{
+					Code: response.PERMISSION_NOT_ALLOWED,
+					Msg:  fmt.Sprintf("您没有权限进行该操作,缺少权限【%s】", sdk_constants.MechanismStaffPermissionNames[routePermission].Name),
+				})
+				return
+			}
+		}
+		c.Next()
+	}
+}

BIN
model/.DS_Store


+ 108 - 0
model/migration.go

@@ -0,0 +1,108 @@
+package model
+
+import (
+	"fmt"
+	"os"
+
+	"gorm.io/driver/mysql"
+	"gorm.io/gorm"
+	"gorm.io/gorm/schema"
+)
+
+var DB *gorm.DB
+
+func Construct(isMigrate bool) {
+	var err error
+	DB, err = gorm.Open(mysql.Open(fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
+		os.Getenv("DB_USERNAME"),
+		os.Getenv("DB_PASSWORD"),
+		os.Getenv("DB_HOST"),
+		os.Getenv("DB_PORT"),
+		os.Getenv("DB_DATABASE"))),
+		&gorm.Config{
+			NamingStrategy: schema.NamingStrategy{
+				SingularTable: true,
+				TablePrefix:   os.Getenv("DB_PREFIX"),
+			},
+			DisableForeignKeyConstraintWhenMigrating: true,
+			SkipDefaultTransaction:                   true,
+			QueryFields:                              true,
+		})
+	if err != nil {
+		panic(err)
+	}
+
+	// debug模式输出所有sql语句
+	// if os.Getenv("APP_DEBUG") == "true" {
+	DB = DB.Debug()
+	// }
+
+	sqlDB, err := DB.DB()
+	if err != nil {
+		panic(err)
+	}
+	sqlDB.SetMaxIdleConns(10)
+	sqlDB.SetMaxOpenConns(100)
+
+	// 开始迁移
+	if isMigrate {
+		Migrate()
+	}
+}
+
+func Migrate() {
+	// 问卷
+	DB.Set("gorm:table_options", "ENGINE=InnoDB default charset = utf8mb4 COLLATE=utf8mb4_0900_as_cs").AutoMigrate(&Survey{})
+	// 问卷授权
+	DB.Set("gorm:table_options", "ENGINE=InnoDB default charset = utf8mb4 COLLATE=utf8mb4_0900_as_cs").AutoMigrate(&SurveyMechanism{})
+	// 问卷结果
+	DB.Set("gorm:table_options", "ENGINE=InnoDB default charset = utf8mb4 COLLATE=utf8mb4_0900_as_cs").AutoMigrate(&SurveyResult{})
+
+	DB.Set("gorm:table_options", "ENGINE=InnoDB default charset = utf8mb4 COLLATE=utf8mb4_0900_as_cs").AutoMigrate(&QuestionnaireSubject{})
+	DB.Set("gorm:table_options", "ENGINE=InnoDB default charset = utf8mb4 COLLATE=utf8mb4_0900_as_cs").AutoMigrate(&QuestionnaireSurvey{})
+	DB.Set("gorm:table_options", "ENGINE=InnoDB default charset = utf8mb4 COLLATE=utf8mb4_0900_as_cs").AutoMigrate(&QuestionnaireSurveyQuestionnaireSubject{})
+	DB.Set("gorm:table_options", "ENGINE=InnoDB default charset = utf8mb4 COLLATE=utf8mb4_0900_as_cs").AutoMigrate(&QuestionnaireTemplate{})
+	DB.Set("gorm:table_options", "ENGINE=InnoDB default charset = utf8mb4 COLLATE=utf8mb4_0900_as_cs").AutoMigrate(&QuestionnaireTemplateSubject{})
+}
+
+// 分页查询的Scope
+func Paginate(page int, pageSize int) func(db *gorm.DB) *gorm.DB {
+	return func(db *gorm.DB) *gorm.DB {
+		if page == 0 {
+			page = 1
+		}
+		switch {
+		case pageSize > 100:
+			pageSize = 100
+		case pageSize <= 0:
+			pageSize = 10
+		}
+
+		offset := (page - 1) * pageSize
+		return db.Offset(offset).Limit(pageSize)
+	}
+}
+
+// 分页查询的Scope
+func Limit(limit int) func(db *gorm.DB) *gorm.DB {
+	return func(db *gorm.DB) *gorm.DB {
+		switch {
+		case limit > 100:
+			limit = 100
+		case limit <= 0:
+			limit = 10
+		}
+
+		return db.Limit(limit)
+	}
+}
+
+// 指定机构的Scope
+func MechanismQuery(mechanismId string) func(db *gorm.DB) *gorm.DB {
+	return func(db *gorm.DB) *gorm.DB {
+		if mechanismId != "" {
+			return db.Where("mechanism_id = ?", mechanismId)
+		}
+		return db
+	}
+}

+ 24 - 0
model/questionnaire_subject.go

@@ -0,0 +1,24 @@
+package model
+
+import (
+	"time"
+
+	"gorm.io/gorm"
+)
+
+type QuestionnaireSubject struct {
+	ID        int64          `gorm:"type:int(20);primaryKey;autoIncrement;comment:ID;" json:"id"`
+	SN        string         `gorm:"type:varchar(255);not null;comment:编号" json:"sn"`
+	Type      int            `gorm:"type:int(11);comment:类型;default:1;" json:"type"`
+	Title     string         `gorm:"type:varchar(255);not null;comment:标题" json:"title"`
+	Validator string         `gorm:"type:longText;nullable;comment:校验器" json:"validator"`
+	Remark    string         `gorm:"type:varchar(255);default:'';comment:备注" json:"remark"`
+	Mark      string         `gorm:"type:varchar(255);default:'';comment:标记" json:"mark"`
+	DeletedAt gorm.DeletedAt `gorm:"column:deleted_at" json:"-"`
+	CreatedAt time.Time      `gorm:"column:created_at" json:"createdAt"`
+	UpdatedAt time.Time      `gorm:"column:updated_at" json:"updatedAt"`
+}
+
+func (s *QuestionnaireSubject) AfterFind(tx *gorm.DB) (err error) {
+	return
+}

+ 19 - 0
model/questionnaire_survey_questionnaire_subject.go

@@ -0,0 +1,19 @@
+package model
+
+import (
+	"gorm.io/gorm"
+)
+
+type QuestionnaireSurveyQuestionnaireSubject struct {
+	ID         int64                `gorm:"type:int(20);primaryKey;autoIncrement;comment:ID;" json:"id"`
+	SurveyID   int64                `gorm:"type:int(20);not null;comment:问卷ID" json:"surveyId"`
+	SubjectID  int64                `gorm:"type:int(20);not null;comment:题目ID" json:"subjectId"`
+	IsRequired int                  `gorm:"type:int(1);comment:是否必填;default:1;" json:"isRequired"`
+	Sort       int64                `gorm:"type:int(11);comment:排序;default:0;" json:"sort"`
+	Survey     QuestionnaireSurvey  `gorm:"foreignKey:SurveyID;references:ID" json:"survey"`
+	Subject    QuestionnaireSubject `gorm:"foreignKey:SubjectID;references:ID" json:"subject"`
+}
+
+func (o *QuestionnaireSurveyQuestionnaireSubject) AfterFind(tx *gorm.DB) (err error) {
+	return
+}

+ 41 - 0
model/questionnaire_suvey.go

@@ -0,0 +1,41 @@
+package model
+
+import (
+	"time"
+
+	"gorm.io/gorm"
+)
+
+const (
+	QUESTIONNAIRE_SURVEY_STATUS_ENABLE  = 1 // 启用
+	QUESTIONNAIRE_SURVEY_STATUS_DISABLE = 2 // 禁用
+
+	QUESTIONNAIRE_SURVEY_TYPE_NORMAL = 1 // 决策问卷
+)
+
+type QuestionnaireSurvey struct {
+	ID             int64                                      `gorm:"type:int(20);primaryKey;autoIncrement;comment:ID;" json:"id"`
+	Title          string                                     `gorm:"type:varchar(255);not null;comment:标题" json:"title"`
+	SN             string                                     `gorm:"type:varchar(255);comment:问卷编号;default:'';" json:"sn"`
+	CanSkipIntro   int                                        `gorm:"type:int(1);comment:是否可以跳过问卷介绍;default:1;" json:"canSkipIntro"`
+	CanSkipResult  int                                        `gorm:"type:int(1);comment:是否可以跳过问卷结果;default:1;" json:"canSkipResult"`
+	GuestAvatar    string                                     `gorm:"type:varchar(255);comment:访客头像;default:'';" json:"guestAvatar"`
+	CustomerAvatar string                                     `gorm:"type:varchar(255);comment:客服头像;default:'';" json:"customerAvatar"`
+	GreetingText   string                                     `gorm:"type:text;comment:问卷提示语;nullable;" json:"greetingText"`
+	FinishedText   string                                     `gorm:"type:text;comment:问卷结束语;nullable;" json:"finishedText"`
+	Intro          string                                     `gorm:"type:longText;comment:问卷介绍;nullable;" json:"intro"`
+	Status         int                                        `gorm:"type:int(1);comment:状态;default:1;" json:"status"`
+	Returns        int                                        `gorm:"type:int(11);comment:返回次数;default:0;" json:"returns"`
+	Dsl            string                                     `gorm:"type:longText;comment:问卷DSL;nullable;" json:"dsl"`
+	Type           int                                        `gorm:"type:int(1);comment:问卷类型;default:1;" json:"type"`
+	Peg            string                                     `gorm:"type:longText;comment:PEG.js;nullable;" json:"peg"`
+	Remark         string                                     `gorm:"type:longText;comment:备注;nullable;" json:"remark"`
+	Subjects       []*QuestionnaireSurveyQuestionnaireSubject `gorm:"foreignKey:SurveyID;references:ID" json:"subjects"`
+	DeletedAt      gorm.DeletedAt                             `gorm:"column:deleted_at" json:"-"`
+	CreatedAt      time.Time                                  `gorm:"column:created_at" json:"createdAt"`
+	UpdatedAt      time.Time                                  `gorm:"column:updated_at" json:"updatedAt"`
+}
+
+func (s *QuestionnaireSurvey) AfterFind(tx *gorm.DB) (err error) {
+	return
+}

+ 22 - 0
model/questionnaire_template.go

@@ -0,0 +1,22 @@
+package model
+
+import (
+	"time"
+
+	"gorm.io/gorm"
+)
+
+type QuestionnaireTemplate struct {
+	ID               int64                           `gorm:"type:int(20);primaryKey;autoIncrement;comment:ID;" json:"id"`
+	SN               string                          `gorm:"type:varchar(255);not null;comment:编号" json:"sn"`
+	Title            string                          `gorm:"type:varchar(255);not null;comment:标题" json:"title"`
+	Peg              string                          `gorm:"type:longText;not null;comment:校验器" json:"peg"`
+	SubjectRelations []*QuestionnaireTemplateSubject `gorm:"foreignKey:TemplateID;References:ID" json:"subjectRelations"`
+	DeletedAt        gorm.DeletedAt                  `gorm:"column:deleted_at" json:"-"`
+	CreatedAt        time.Time                       `gorm:"column:created_at" json:"createdAt"`
+	UpdatedAt        time.Time                       `gorm:"column:updated_at" json:"updatedAt"`
+}
+
+func (s *QuestionnaireTemplate) AfterFind(tx *gorm.DB) (err error) {
+	return
+}

+ 18 - 0
model/questionnaire_template_subject.go

@@ -0,0 +1,18 @@
+package model
+
+import (
+	"gorm.io/gorm"
+)
+
+type QuestionnaireTemplateSubject struct {
+	ID         int64                 `gorm:"type:int(20);primaryKey;autoIncrement;comment:ID;" json:"id"`
+	TemplateID int64                 `gorm:"type:int(20);not null;comment:模板ID" json:"templateId"`
+	SubjectID  int64                 `gorm:"type:int(20);not null;comment:题目ID" json:"subjectId"`
+	Sort       int64                 `gorm:"type:int(11);comment:排序;default:0;" json:"sort"`
+	Template   QuestionnaireTemplate `gorm:"foreignKey:TemplateID;references:ID" json:"template"`
+	Subject    QuestionnaireSubject  `gorm:"foreignKey:SubjectID;references:ID" json:"subject"`
+}
+
+func (o *QuestionnaireTemplateSubject) AfterFind(tx *gorm.DB) (err error) {
+	return
+}

+ 56 - 0
model/survey.go

@@ -0,0 +1,56 @@
+package model
+
+import (
+	"os"
+	"surveyService/util"
+	"time"
+
+	"gorm.io/gorm"
+)
+
+const SurveyTableName = "survey"
+
+const (
+	SURVEY_STATUS_ENABLE  = 1 // 可用
+	SURVEY_STATUS_DISABLE = 2 // 禁用
+
+	SURVEY_TYPE_NORMAL   = 1 // 普通文件
+	SURVEY_TYPE_PHYSICAL = 2 // 定制体检
+)
+
+type Survey struct {
+	ID         int64          `gorm:"type:int(20);autoIncrement;comment:ID;" json:"id"`
+	SN         string         `gorm:"type:varchar(255);comment:问卷编号;default:'';index;" json:"sn"`
+	SurveyCode string         `gorm:"type:varchar(255);comment:问卷编码;default:'';" json:"surveyCode"`
+	Name       string         `gorm:"type:varchar(255);comment:问卷名称;default:'';" json:"name"`
+	Cover      string         `gorm:"type:varchar(255);comment:封面;default:'';" json:"cover"`
+	Status     int            `gorm:"type:int(11);comment:状态:1:可用,2:禁用;default:2;" json:"status"`
+	Remark     string         `gorm:"type:mediumText;comment:备注;nullable;" json:"remark"`
+	Type       int64          `gorm:"type:int(11);comment:问卷类型:1:问卷,2:定制体检;default:1;" json:"type"`
+	DeletedAt  gorm.DeletedAt `gorm:"column:deleted_at" json:"-"`
+	CreatedAt  time.Time      `gorm:"column:created_at" json:"createdAt"`
+	UpdatedAt  time.Time      `gorm:"column:updated_at" json:"updatedAt"`
+}
+
+func (u *Survey) AfterCreate(tx *gorm.DB) (err error) {
+	tx.Model(u).Update("sn", u.GetHashId(u.ID))
+	return
+}
+
+func (u *Survey) AfterFind(tx *gorm.DB) (err error) {
+	return
+}
+
+func (u *Survey) GetHashId(id int64) string {
+	return util.GetHashId(id, SurveyTableName)
+}
+
+func (u *Survey) GetRawId(sn string) int64 {
+	rawId, _ := util.GetIdByHashId(sn, SurveyTableName)
+	return rawId
+}
+
+func (u *Survey) TableName() string {
+	dbPrefix := os.Getenv("DB_PREFIX")
+	return dbPrefix + SurveyTableName
+}

+ 61 - 0
model/survey_mechanism.go

@@ -0,0 +1,61 @@
+package model
+
+import (
+	"os"
+	"surveyService/util"
+	"time"
+
+	"gorm.io/gorm"
+)
+
+const SurveyMechanismTableName = "survey_mechanism"
+
+const (
+	SURVEY_MECHANISM_AUTHORIZE_STATUS_ENABLE  = 1 // 可用
+	SURVEY_MECHANISM_AUTHORIZE_STATUS_DISABLE = 2 // 禁用
+
+	SURVEY_MECHANISM_STATUS_ENABLE  = 1 // 可用
+	SURVEY_MECHANISM_STATUS_DISABLE = 2 // 禁用
+)
+
+type SurveyMechanism struct {
+	ID              int64           `gorm:"type:int(20);autoIncrement;comment:ID;" json:"id"`
+	SN              string          `gorm:"type:varchar(255);comment:问卷编号;default:'';" json:"sn"`
+	AuthorizeStatus int             `gorm:"type:int(11);comment:状态:1:可用,2:禁用;default:2;" json:"status"`
+	Status          int             `gorm:"type:int(11);comment:是否启用:1:启用,2:禁用;default:2;" json:"enable"`
+	MechanismId     string          `gorm:"type:varchar(255);comment:机构编号;default:'';" json:"mechanismId"`
+	SurveyId        int64           `gorm:"type:int(20);comment:问卷ID;default:0;" json:"surveyId"`
+	Permissions     string          `gorm:"type:mediumText;comment:权限;nullable;" json:"permissions"`
+	Name            string          `gorm:"type:varchar(255);comment:问卷名称;default:'';" json:"name"`
+	Cover           string          `gorm:"type:varchar(255);comment:封面;default:'';" json:"cover"`
+	Description     string          `gorm:"type:mediumText;comment:描述;nullable;" json:"description"`
+	Survey          Survey          `gorm:"foreignKey:SurveyId;references:ID" json:"survey"`
+	SurveyResults   []*SurveyResult `gorm:"foreignKey:SurveyMechanismId;references:ID" json:"surveyResults"`
+	SurveyResult    *SurveyResult   `gorm:"foreignKey:SurveyMechanismId;references:ID" json:"surveyResult"`
+	DeletedAt       gorm.DeletedAt  `gorm:"column:deleted_at" json:"-"`
+	CreatedAt       time.Time       `gorm:"column:created_at" json:"createdAt"`
+	UpdatedAt       time.Time       `gorm:"column:updated_at" json:"updatedAt"`
+}
+
+func (u *SurveyMechanism) AfterCreate(tx *gorm.DB) (err error) {
+	tx.Model(u).Update("sn", u.GetHashId(u.ID))
+	return
+}
+
+func (u *SurveyMechanism) AfterFind(tx *gorm.DB) (err error) {
+	return
+}
+
+func (u *SurveyMechanism) GetHashId(id int64) string {
+	return util.GetHashId(id, SurveyMechanismTableName)
+}
+
+func (u *SurveyMechanism) GetRawId(sn string) int64 {
+	rawId, _ := util.GetIdByHashId(sn, SurveyMechanismTableName)
+	return rawId
+}
+
+func (u *SurveyMechanism) TableName() string {
+	dbPrefix := os.Getenv("DB_PREFIX")
+	return dbPrefix + SurveyMechanismTableName
+}

+ 63 - 0
model/survey_result.go

@@ -0,0 +1,63 @@
+package model
+
+import (
+	"os"
+	"surveyService/util"
+	"time"
+
+	"gorm.io/gorm"
+)
+
+const SurveyResultTableName = "survey_result"
+
+const (
+	SurveyResultMethodMechanism = 1 // 机构端
+	SurveyResultMethodArchives  = 2 // 用户端
+	SurveyResultMethodManage    = 3 // 平台后台
+
+	SurveyResultStatusWait   = 1 // 填写中
+	SurveyResultStatusFulled = 2 // 已完善
+	SurveyResultStatusDone   = 3 // 已处理
+)
+
+type SurveyResult struct {
+	ID                int64            `gorm:"type:int(20);autoIncrement;comment:ID;" json:"id"`
+	SN                string           `gorm:"type:varchar(255);comment:问卷结果编号;default:'';" json:"sn"`
+	Method            int              `gorm:"type:int(11);comment:调查方式;default:1;" json:"method"`
+	StartTime         time.Time        `gorm:"type:datetime;comment:开始时间;default:null;" json:"startTime"`
+	EndTime           time.Time        `gorm:"type:datetime;comment:结束时间;default:null;" json:"endTime"`
+	ArchivesId        string           `gorm:"type:varchar(255);comment:档案ID;default:'';" json:"archivesId"`
+	AnswerRaw         string           `gorm:"type:mediumText;comment:答案原始数据;nullable;" json:"answerRaw"`
+	ResultRaw         string           `gorm:"type:mediumText;comment:结果原始数据;nullable;" json:"resultRaw"`
+	Status            int              `gorm:"type:int(11);comment:状态;default:1;" json:"status"`
+	Remark            string           `gorm:"type:mediumText;comment:备注;nullable;" json:"remark"`
+	SurveyMechanismId int64            `gorm:"type:int(20);comment:机构问卷ID;default:0;" json:"surveyMechanismId"`
+	SurveyMechanism   *SurveyMechanism `gorm:"foreignKey:SurveyMechanismId;references:ID" json:"surveyMechanism"`
+	MechanismId       string           `gorm:"type:varchar(255);comment:机构ID;default:'';" json:"mechanismId"`
+	Extra             string           `gorm:"type:varchar(255);comment:自定义参数;default:'';" json:"extra"`
+	DeletedAt         gorm.DeletedAt   `gorm:"column:deleted_at" json:"-"`
+	CreatedAt         time.Time        `gorm:"column:created_at" json:"createdAt"`
+	UpdatedAt         time.Time        `gorm:"column:updated_at" json:"updatedAt"`
+}
+
+func (u *SurveyResult) AfterCreate(tx *gorm.DB) (err error) {
+	tx.Model(u).Update("sn", u.GetHashId(u.ID))
+	return
+}
+
+func (u *SurveyResult) AfterFind(tx *gorm.DB) (err error) {
+	return
+}
+
+func (u *SurveyResult) GetHashId(id int64) string {
+	return util.GetHashId(id, SurveyResultTableName)
+}
+
+func (u *SurveyResult) GetRawId(sn string) int64 {
+	rawId, _ := util.GetIdByHashId(sn, SurveyResultTableName)
+	return rawId
+}
+func (u *SurveyResult) TableName() string {
+	dbPrefix := os.Getenv("DB_PREFIX")
+	return dbPrefix + SurveyResultTableName
+}

+ 41 - 0
model/system_setting.go

@@ -0,0 +1,41 @@
+package model
+
+import (
+	"time"
+
+	"gorm.io/gorm"
+)
+
+const (
+	SystemSettingTableName = "system_setting" // 表名
+)
+
+const (
+	SystemSettingKeyAlgorAppKey    = "AlgorAppKey"
+	SystemSettingKeyAlgorAppSecret = "AlgorAppSecret"
+	SystemSettingKeyAlgorDomain    = "AlgorDomain"
+	SystemSettingKeyAlgorDefaultSn = "AlgorDefaultSn"
+)
+
+type SystemSetting struct {
+	ID        int64          `gorm:"type:int(20);autoIncrement;comment:ID;" json:"id"`
+	Key       string         `gorm:"type:varchar(255);comment:KEY;" json:"key"`
+	Value     string         `gorm:"type:varchar(255);comment:VALUE;" json:"value"`
+	DeletedAt gorm.DeletedAt `gorm:"column:deleted_at" json:"-"`
+	CreatedAt time.Time      `gorm:"column:created_at" json:"createdAt"`
+	UpdatedAt time.Time      `gorm:"column:updated_at" json:"updatedAt"`
+}
+
+func (u *SystemSetting) AfterCreate(tx *gorm.DB) (err error) {
+	// tx.Model(u).Update("sn", util.GetHashId(u.ID, MechanismTableName))
+	return
+}
+
+func (u *SystemSetting) AfterFind(tx *gorm.DB) (err error) {
+	return
+}
+
+func (u *SystemSetting) TableName() string {
+	dbPrefix := ""
+	return dbPrefix + SystemSettingTableName
+}

+ 35 - 0
model/types/types.go

@@ -0,0 +1,35 @@
+package types
+
+import (
+	"database/sql"
+	"encoding/json"
+	"fmt"
+	"time"
+)
+
+type NullTime struct {
+	sql.NullTime
+}
+
+func (v NullTime) MarshalJSON() ([]byte, error) {
+	if v.Valid {
+		return json.Marshal(v.Time)
+	} else {
+		return json.Marshal(nil)
+	}
+}
+
+func (v *NullTime) UnmarshalJSON(data []byte) error {
+	var s *time.Time
+	if err := json.Unmarshal(data, &s); err != nil {
+		return err
+	}
+	if s != nil {
+		v.Valid = true
+		v.Time = *s
+		fmt.Println(s)
+	} else {
+		v.Valid = false
+	}
+	return nil
+}

+ 67 - 0
response/errcode.go

@@ -0,0 +1,67 @@
+package response
+
+type ErrCode struct {
+	Code int
+	Msg  string
+}
+
+const (
+	SUCCESS               = 200
+	AUTHORIZATION_EXPIRED = 401
+	INVALID_TOKEN         = 403
+	NOT_FOUND             = 404
+	ERROR                 = 500
+
+	PLATFORM_ERROR               = 1001 // 第三方错误
+	REDIS_ERROR                  = 1002 // 缓存错误
+	VALIDATOR_ERROR              = 1003 // validator校验器错误
+	DEBUG_ERROR                  = 1004 // DEBUG的错误信息
+	DB_ERROR                     = 1005 // DB异常
+	PERMISSION_NOT_ALLOWED       = 1006
+	MECHANISM_LOST               = 1007 // 机构ID没传
+	DATA_NOT_FOUND               = 9000
+	NEED_REGIST                  = 9001
+	SIGN_ERROR                   = 9002
+	ACCOUNT_FORBID               = 9003
+	MEMBER_PASSWORD_NOT_SET      = 9004 // 密码未设置
+	MEMBER_MOBILE_NOT_BIND       = 9005 // 手机号未绑定
+	MEMBER_USERINFO_NOT_COMPLETE = 9006 // 用户信息未完善
+
+	USER_MOBILE_NOT_BIND = 10002
+)
+
+var ErrorCodeMessage = map[int]string{
+	SUCCESS:                "操作成功",
+	ERROR:                  "服务器异常",
+	NOT_FOUND:              "无操作权限",
+	AUTHORIZATION_EXPIRED:  "凭证已失效,请重新登录",
+	INVALID_TOKEN:          "凭证无效,请重新登录",
+	PLATFORM_ERROR:         "第三方调用异常",
+	REDIS_ERROR:            "服务器异常[1002]",
+	VALIDATOR_ERROR:        "参数输入错误,请仔细检查",
+	DEBUG_ERROR:            "服务器异常",
+	DB_ERROR:               "服务器异常[1005]",
+	PERMISSION_NOT_ALLOWED: "您没有权限进行该操作",
+	DATA_NOT_FOUND:         "该项不存在",
+	NEED_REGIST:            "服务人员不存在,请先补充资料",
+	SIGN_ERROR:             "签名错误",
+	ACCOUNT_FORBID:         "您的账号已被禁用",
+}
+
+var (
+	ErrSuccess              = &ErrCode{Code: SUCCESS, Msg: ErrorCodeMessage[SUCCESS]}
+	Err                     = &ErrCode{Code: ERROR, Msg: ErrorCodeMessage[ERROR]}
+	ErrNotFound             = &ErrCode{Code: NOT_FOUND, Msg: ErrorCodeMessage[NOT_FOUND]}
+	ErrAuthorizationExpired = &ErrCode{Code: AUTHORIZATION_EXPIRED, Msg: ErrorCodeMessage[AUTHORIZATION_EXPIRED]}
+	ErrInvalidToken         = &ErrCode{Code: INVALID_TOKEN, Msg: ErrorCodeMessage[INVALID_TOKEN]}
+	ErrPlatform             = &ErrCode{Code: PLATFORM_ERROR, Msg: ErrorCodeMessage[PLATFORM_ERROR]}
+	ErrRedis                = &ErrCode{Code: REDIS_ERROR, Msg: ErrorCodeMessage[REDIS_ERROR]}
+	ErrValidator            = &ErrCode{Code: VALIDATOR_ERROR, Msg: ErrorCodeMessage[VALIDATOR_ERROR]}
+	ErrDebug                = &ErrCode{Code: DEBUG_ERROR, Msg: ErrorCodeMessage[DEBUG_ERROR]}
+	ErrDatabase             = &ErrCode{Code: DB_ERROR, Msg: ErrorCodeMessage[DB_ERROR]}
+	ErrPermissionNotAllowed = &ErrCode{Code: PERMISSION_NOT_ALLOWED, Msg: ErrorCodeMessage[PERMISSION_NOT_ALLOWED]}
+	ErrDataNotFound         = &ErrCode{Code: DATA_NOT_FOUND, Msg: ErrorCodeMessage[DATA_NOT_FOUND]}
+	ErrNeedRegist           = &ErrCode{Code: NEED_REGIST, Msg: ErrorCodeMessage[NEED_REGIST]}
+	ErrSign                 = &ErrCode{Code: SIGN_ERROR, Msg: ErrorCodeMessage[SIGN_ERROR]}
+	ErrAccountForbid        = &ErrCode{Code: ACCOUNT_FORBID, Msg: ErrorCodeMessage[ACCOUNT_FORBID]}
+)

+ 76 - 0
response/response.go

@@ -0,0 +1,76 @@
+package response
+
+import (
+	"net/http"
+	"strings"
+
+	"surveyService/util/validator"
+
+	"github.com/gin-gonic/gin"
+)
+
+type JSONResult[T any] struct {
+	Code int    `json:"code"`
+	Msg  string `json:"message"`
+	Data T      `json:"data"`
+}
+type ListResponse[T any] struct {
+	List []*T `json:"list"`
+}
+type PaginateResponse[T any] struct {
+	List  []*T `json:"list"`
+	Total int  `json:"total"`
+}
+type OffsetResponse[T any] struct {
+	List   []*T `json:"list"`
+	Offset any  `json:"offset"`
+}
+type OffsetTotalResponse[T any] struct {
+	List   []*T  `json:"list"`
+	Total  int64 `json:"total"`
+	Offset any   `json:"offset"`
+}
+
+// 成功响应
+func Success[T any](ctx *gin.Context, data T) {
+	ctx.JSON(http.StatusOK, &JSONResult[T]{
+		Code: ErrSuccess.Code,
+		Msg:  ErrSuccess.Msg,
+		Data: data,
+	})
+}
+
+// 其他响应
+func Response[T any](ctx *gin.Context, data T) {
+	ctx.JSON(http.StatusOK, &data)
+}
+
+// 错误响应
+func Fail(c *gin.Context, errcode *ErrCode, msg ...string) {
+	var errMsg string
+	if len(msg) > 0 {
+		errMsg = strings.Join(msg, "")
+	} else {
+		//错误提示优化, 增加错误展示方式
+		errMsg = errcode.Msg
+	}
+	var T any = struct{}{}
+	c.JSON(http.StatusOK, &JSONResult[any]{
+		Code: errcode.Code,
+		Msg:  errMsg,
+		Data: T,
+	})
+	c.Abort()
+}
+
+// 错误响应
+func FailValidator(c *gin.Context, err error) {
+	var T any = struct{}{}
+	validatorError := validator.TranslateError(err)
+	c.JSON(http.StatusOK, &JSONResult[any]{
+		Code: VALIDATOR_ERROR,
+		Msg:  validatorError.Error(),
+		Data: T,
+	})
+	c.Abort()
+}

+ 164 - 0
router/router.go

@@ -0,0 +1,164 @@
+package router
+
+import (
+	"net/http"
+
+	"surveyService/controller/callback"
+	"surveyService/controller/questionnaire_subject"
+	"surveyService/controller/questionnaire_survey"
+	"surveyService/controller/questionnaire_template"
+	"surveyService/controller/survey"
+	"surveyService/controller/survey_mechanism"
+	"surveyService/controller/survey_result"
+	"surveyService/middleware"
+
+	"github.com/gin-gonic/gin"
+	"gogs.uu.mdfitnesscao.com/hys/sdk/constants"
+)
+
+func Init() *gin.Engine {
+	router := gin.Default()
+	router.Use(middleware.Cors())
+
+	router.GET("/ping", func(c *gin.Context) {
+		c.String(http.StatusOK, "The Server is Running")
+	})
+
+	// 后台API
+	backendApiRouter := router.Group("/backend")
+	backendApiRouter.Use(middleware.Authorize())
+	{
+		// |------ 问卷管理
+		// 获取问卷列表
+		backendApiRouter.GET("/survey/list", survey.List)
+		// 创建问卷
+		backendApiRouter.POST("/survey/updateOrCreate", middleware.PermissionCheck(constants.UserPermissionSurveyEdit), survey.UpdateOrCreate)
+		// 获取问卷详情
+		backendApiRouter.GET("/survey/detail", middleware.PermissionCheck(constants.UserPermissionSurveyView), survey.Detail)
+		// 获取问卷列表(分页)
+		backendApiRouter.GET("/survey/paginate", middleware.PermissionCheck(constants.UserPermissionSurveyView), survey.Paginate)
+		// 修改问卷备注
+		backendApiRouter.POST("/survey/update/remark", middleware.PermissionCheck(constants.UserPermissionSurveyEdit), survey.UpdateRemark)
+		// 修改问卷状态
+		backendApiRouter.POST("/survey/update/status", middleware.PermissionCheck(constants.UserPermissionSurveyEdit), survey.UpdateStatus)
+
+		// |------ 问卷授权管理
+		// 创建问卷授权
+		backendApiRouter.POST("/surveyAuthorize/create", middleware.PermissionCheck(constants.UserPermissionSurveyAuthorizeEdit), survey_mechanism.CreateForManage)
+		// 获取问卷已被授权的机构列表
+		backendApiRouter.GET("/surveyAuthorize/listMechanismIds", middleware.PermissionCheck(constants.UserPermissionSurveyAuthorizeView), survey_mechanism.ListMechanismIdsForSurveyForManage)
+		// 获取授权列表
+		backendApiRouter.GET("/surveyAuthorize/paginate", middleware.PermissionCheck(constants.UserPermissionSurveyAuthorizeView), survey_mechanism.PaginateForManage)
+		// 修改授权状态
+		backendApiRouter.POST("/surveyAuthorize/update/status", middleware.PermissionCheck(constants.UserPermissionSurveyAuthorizeEdit), survey_mechanism.UpdateAuthorizeStatusForManage)
+
+		// |------ 问卷结果管理
+		// 获取问卷结果列表
+		backendApiRouter.GET("/surveyResult/paginate", middleware.PermissionCheck(constants.UserPermissionSurveyResultView), survey_result.PaginateForManage)
+		// 获取问卷结果详情
+		backendApiRouter.GET("/surveyResult/detail", middleware.PermissionCheck(constants.UserPermissionSurveyResultView), survey_result.DetailForManage)
+		// 重新执行一次算法
+		backendApiRouter.POST("/surveyResult/run", middleware.PermissionCheck(constants.UserPermissionSurveyResultEdit), survey_result.RunForManage)
+
+		// |------基础问卷管理
+		// 获取问卷列表(不要权限)
+		backendApiRouter.GET("/questionnaireSurvey/list", questionnaire_survey.List)
+
+		// 获取问题模板列表
+		backendApiRouter.GET("/algor/questionnaireTemplate/list", questionnaire_template.List)
+		// 获取问题库列表
+		backendApiRouter.GET("/algor/questionnaireSubject/list", questionnaire_subject.List)
+		// 将问题库和问题模板缓存到Redis
+		backendApiRouter.GET("/algor/questionnaire/cache", questionnaire_survey.CacheList)
+	}
+
+	// 机构API
+	mechanismApiRouter := router.Group("/mechanism")
+	mechanismApiRouter.Use(middleware.AuthorizeMechanism())
+	{
+		// |------ 问卷管理
+		// 获取问卷列表
+		mechanismApiRouter.GET("/survey/paginate", middleware.PermissionCheckStaff(constants.MechanismStaffPermissionSurveyView), survey_mechanism.Paginate)
+		// 修改问卷状态
+		mechanismApiRouter.POST("/survey/update/status", middleware.PermissionCheckStaff(constants.MechanismStaffPermissionSurveyEdit), survey_mechanism.UpdateStatus)
+		// 修改问卷信息
+		mechanismApiRouter.POST("/survey/update", middleware.PermissionCheckStaff(constants.MechanismStaffPermissionSurveyEdit), survey_mechanism.Update)
+		// 获取问卷详情
+		mechanismApiRouter.GET("/survey/detail", middleware.PermissionCheckStaff(constants.MechanismStaffPermissionSurveyView), survey_mechanism.Detail)
+		// 获取问卷详情(转译系统)
+		mechanismApiRouter.GET("/survey/detail/config", middleware.PermissionCheckStaff(constants.MechanismStaffPermissionSurveyView), survey_mechanism.DetailSurveyConfig)
+
+		// |------ 问卷结果管理
+		// 获取问卷结果列表
+		mechanismApiRouter.GET("/surveyResult/paginate", middleware.PermissionCheckStaff(constants.MechanismStaffPermissionSurveyResultView), survey_result.Paginate)
+		// 获取问卷结果详情
+		mechanismApiRouter.GET("/surveyResult/detail", middleware.PermissionCheckStaff(constants.MechanismStaffPermissionSurveyResultView), survey_result.Detail)
+
+		// |------基础问卷管理
+		// 获取问卷列表(不要权限)
+		mechanismApiRouter.GET("/questionnaireSurvey/list", questionnaire_survey.List)
+
+		// 获取问题模板列表
+		mechanismApiRouter.GET("/algor/questionnaireTemplate/list", questionnaire_template.List)
+		// 获取问题库列表
+		mechanismApiRouter.GET("/algor/questionnaireSubject/list", questionnaire_subject.List)
+	}
+
+	memberApiRouter := router.Group("/member")
+	memberApiRouter.Use(middleware.MemberMechanism(), middleware.AuthorizeMember())
+	{
+		// |------ 问卷
+		// 获取问卷列表
+		memberApiRouter.GET("/survey/list", survey_mechanism.ListForMember)
+		// 获取我的评测记录
+		memberApiRouter.GET("/surveyResult/paginate", survey_result.PaginateForMember)
+	}
+
+	// 下面的接口支持用户端使用,支持第三方凭证使用
+	// 鉴于middleware的执行顺序,AuthorizeMember必须放在AuthorizeSurveyToken后面,否则会导致AuthorizeSurveyToken校验无效
+	memberApiRouter.Use(middleware.MemberMechanism(), middleware.AuthorizeSurveyToken(), middleware.AuthorizeMember())
+	{
+		// |------ 问卷
+		// 获取问卷详情
+		memberApiRouter.GET("/survey/detail", survey_mechanism.DetailForMember)
+		// 获取问卷题目列表
+		memberApiRouter.GET("/survey/detail/questions", survey_mechanism.DetailQuestionsForMember)
+		// 创建答题结果
+		memberApiRouter.POST("/surveyResult/create", survey_result.CreateSurveyResultForMember)
+		// 提交答题结果
+		memberApiRouter.POST("/surveyResult/submit", survey_result.SubmitSurveyResultForMember)
+		// 创建答题结果
+		memberApiRouter.POST("/surveyResult/createAndSubmit", survey_result.CreateAndSubmitSurveyResultForMember)
+		// 获取问卷结果
+		memberApiRouter.GET("/surveyResult/detail", survey_result.DetailSurveyResultForMember)
+		// 获取最后一份问卷结果
+		memberApiRouter.GET("/surveyResult/detail/last", survey_result.DetailLastSurveyResultForMember)
+	}
+
+	callbackApiRouter := router.Group("/callback")
+	// 问卷结果回调
+	callbackApiRouter.POST("/survey/result", callback.CallbackSurveyResult)
+	callbackApiRouter.POST("/survey/sync", callback.CallbackSurveySync)
+
+	// 外部开发接口
+	externalOpenApiRouter := router.Group("/external/openapi")
+	// 新增机构ID拦截
+	externalOpenApiRouter.Use(middleware.AuthorizeMechanismByAppKey())
+	{
+		// 获取已授权的问卷列表
+		externalOpenApiRouter.POST("/survey/list", survey_mechanism.List)
+		// 通过某个问卷编号获取H5端问卷答题地址
+		externalOpenApiRouter.POST("/survey/link", survey_mechanism.GetLinkBySurveyId)
+		// 通过某个问卷结果编号获取H5端结果页地址
+		externalOpenApiRouter.POST("/survey/result/link", survey_result.GetLinkBySurveyResultId)
+	}
+
+	// 内部开放接口
+	openApiRouter := router.Group("/openapi")
+	// 获取问题模板列表
+	openApiRouter.POST("/questionnaireTemplate/list", questionnaire_template.List)
+	// 获取问题库列表
+	openApiRouter.POST("/questionnaireSubject/list", questionnaire_subject.List)
+
+	return router
+}

+ 291 - 0
sdk/survey_disease/disease_screening.go

@@ -0,0 +1,291 @@
+package survey_disease
+
+import (
+	"strconv"
+	"strings"
+	"time"
+
+	"gogs.uu.mdfitnesscao.com/hys/sdk"
+	"gogs.uu.mdfitnesscao.com/hys/sdk/survey"
+)
+
+// 重大疾病定制体检
+func DiseaseScreeningCal(surveys map[string]*sdk.SurveyAnswer) *SurveyResult {
+	result := SurveyResult{}
+	result.AlTime = time.Now().Format("2006-01-02 15:04:05")
+	info := SurveyResultTemplate{
+		Type: "text",
+		Text: []string{"尊敬的客户,您好!根据您的答题结果,我们为您推荐如下体检项目:"},
+	}
+
+	result.SurveyResultDatas = append(result.SurveyResultDatas, info)
+
+	//错误信息
+	errInfo := SurveyResultTemplate{
+		Type: "text",
+		Text: []string{},
+	}
+
+	if surveys["Gender"] == nil || surveys["Age"] == nil {
+		errInfo.Text = append(errInfo.Text, "没有填写性别或年龄")
+	}
+
+	userInfo := "0"
+	isPregnancy := false //是否备孕
+
+	if survey.CheckAnswerKeysIsValid(*surveys["Gender"], []string{"zH63GN"}) { //男性
+		userInfo = "1"
+	} else if survey.CheckAnswerKeysIsValid(*surveys["Gender"], []string{"M3CRpa"}) { //女性
+		userInfo = "2"
+		if surveys["SexualLife"] != nil {
+			if survey.CheckAnswerKeysIsValid(*surveys["SexualLife"], []string{"J4yF94"}) { //有性生活
+				userInfo = "4"
+			} else {
+				userInfo = "3"
+			}
+		}
+
+		if surveys["Pregnancy"] != nil && survey.CheckAnswerKeysIsValid(*surveys["Pregnancy"], []string{"QhPy89"}) { //有备孕
+			isPregnancy = true
+		}
+	}
+
+	//获取用户年龄
+	age := 0.0
+	if surveys["Age"].InputAnswers.Value[0] != "" {
+		f64, err := strconv.ParseFloat(surveys["Age"].InputAnswers.Value[0], 64)
+		if err == nil {
+			age = f64
+		} else {
+			errInfo.Text = append(errInfo.Text, "未填写年龄或填写格式错误")
+		}
+	}
+
+	sysItemsCopy := make([]Item, len(SysItemsForDiseaseScreening))
+	copy(sysItemsCopy, SysItemsForDiseaseScreening)
+
+	addItems := []string{}
+	userInfoTrans, _ := strconv.Atoi(userInfo)
+
+	if userInfoTrans >= 2 && age >= 35 {
+		addItems = append(addItems, "乳腺X线(钼靶)")
+	}
+
+	//您既往是否存在以下情况?
+	if surveys["PreviousSymptoms"] != nil {
+
+		if survey.CheckAnswerKeysIsValid(*surveys["PreviousSymptoms"], []string{"W1PJRG"}) { //胃食管反流病
+			addItems = append(addItems, "食管内镜")
+		}
+
+		if survey.CheckAnswerKeysIsValid(*surveys["PreviousSymptoms"], []string{"hDNdwW"}) { //幽门螺杆菌感染、慢性胃炎、慢性胃溃疡、胃息肉
+			addItems = append(addItems, "胃镜")
+			addItems = append(addItems, "胃功能")
+		}
+
+		if survey.CheckAnswerKeysIsValid(*surveys["PreviousSymptoms"], []string{"aWpjX8"}) { //溃疡性结肠炎、肠息肉、大肠癌术后、大肠腺瘤治疗后
+			addItems = append(addItems, "肠镜")
+			addItems = append(addItems, "粪便免疫化学测试")
+		}
+
+		if survey.CheckAnswerKeysIsValid(*surveys["PreviousSymptoms"], []string{"0pe2RF"}) { //乙肝病毒感染(乙肝表面抗原阳性)
+			addItems = append(addItems, "乙肝病毒DNA定量")
+		}
+
+		if survey.CheckAnswerKeysIsValid(*surveys["PreviousSymptoms"], []string{"GDjH9Q"}) { //慢性胰腺炎反复发作、胰管结石
+			if userInfoTrans == 5 {
+				addItems = append(addItems, "胰腺MRI")
+			} else {
+				addItems = append(addItems, "胰腺CT/MRI")
+			}
+		}
+
+		if survey.CheckAnswerKeysIsValid(*surveys["PreviousSymptoms"], []string{"X4P1wE"}) { //乳腺导管或小叶不典型增生、乳腺小叶原位癌
+			if userInfoTrans >= 2 {
+				addItems = append(addItems, "乳腺X线(钼靶)")
+			}
+		}
+	}
+
+	//您家族中一级亲属(父母、子女、兄弟姐妹)是否有以下情况?
+	if surveys["FamilySymptoms"] != nil {
+		if survey.CheckAnswerKeysIsValid(*surveys["FamilySymptoms"], []string{"GX8r17"}) { //结直肠癌
+			if surveys["NHrbWb"] != nil {
+				if survey.CheckAnswerKeysIsValid(*surveys["NHrbWb"], []string{"zpjBxZ"}) { //5年内没有做过肠镜
+					addItems = append(addItems, "肠镜")
+					addItems = append(addItems, "粪便免疫化学测试")
+				}
+			}
+		}
+
+		if survey.CheckAnswerKeysIsValid(*surveys["FamilySymptoms"], []string{"5eJrZX"}) { //遗传性大肠癌
+			addItems = append(addItems, "肠镜")
+			addItems = append(addItems, "粪便免疫化学测试")
+			addItems = append(addItems, "肠癌基因检测")
+		}
+
+		if survey.CheckAnswerKeysIsValid(*surveys["FamilySymptoms"], []string{"PXEeGr"}) { //胃癌
+			if surveys["T0A7tp"] != nil {
+				if survey.CheckAnswerKeysIsValid(*surveys["T0A7tp"], []string{"FDD2c3"}) { //5年内没有做过胃镜
+					addItems = append(addItems, "胃镜")
+				}
+			}
+
+		}
+
+		if survey.CheckAnswerKeysIsValid(*surveys["FamilySymptoms"], []string{"eCZYBR"}) { //食管癌
+			addItems = append(addItems, "食管内镜")
+		}
+
+		if survey.CheckAnswerKeysIsValid(*surveys["FamilySymptoms"], []string{"ztm38d"}) { //胰腺癌
+			if userInfoTrans == 5 {
+				addItems = append(addItems, "胰腺MRI")
+			} else {
+				addItems = append(addItems, "胰腺CT/MRI")
+			}
+		}
+
+		if survey.CheckAnswerKeysIsValid(*surveys["FamilySymptoms"], []string{"4Zmn1k"}) { //乳腺癌
+			if userInfoTrans >= 2 {
+				addItems = append(addItems, "乳腺X线(钼靶)")
+				addItems = append(addItems, "BRCA基因检测")
+			}
+		}
+	}
+
+	//您近5年是否做过胃镜检查
+	if surveys["T0A7tp"] != nil {
+		if survey.CheckAnswerKeysIsValid(*surveys["T0A7tp"], []string{"FDD2c3"}) && age >= 35 { //5年内没有做过肠胃镜
+			addItems = append(addItems, "胃镜")
+		} else {
+			if surveys["TnNxjy"] != nil {
+				if !survey.CheckAnswerKeysIsValid(*surveys["TnNxjy"], []string{"hGhT4C"}) { //胃镜有异常
+					addItems = append(addItems, "胃镜")
+				}
+			}
+		}
+
+	}
+
+	//您近5年是否做过肠镜检查
+	if surveys["NHrbWb"] != nil {
+		if survey.CheckAnswerKeysIsValid(*surveys["NHrbWb"], []string{"zpjBxZ"}) && age >= 35 { //5年内没有做过肠胃镜
+			addItems = append(addItems, "肠镜")
+		} else {
+			if surveys["8CyeMS"] != nil {
+				if !survey.CheckAnswerKeysIsValid(*surveys["8CyeMS"], []string{"a4XHkX"}) { //肠镜有异常
+					addItems = append(addItems, "肠镜")
+				}
+			}
+
+		}
+
+	}
+
+	//您近期有无以下症状?(符合其中一项即可)
+	if surveys["Symptom"] != nil {
+		if survey.CheckAnswerKeysIsValid(*surveys["Symptom"], []string{"Q9QGxK"}) { //吞咽不适、哽噎感等
+			addItems = append(addItems, "食管内镜")
+		}
+
+		if survey.CheckAnswerKeysIsValid(*surveys["Symptom"], []string{"rRCbMC"}) { //恶心、呕吐、反复持续的腹痛、腹胀等上腹部不适症状
+			addItems = append(addItems, "胃镜")
+			addItems = append(addItems, "C13呼气试验")
+		}
+
+		if survey.CheckAnswerKeysIsValid(*surveys["Symptom"], []string{"jk16dT"}) { //食欲不振、乏力、腹泻、消瘦、腰背部酸痛、大便乳糜状等
+			if userInfoTrans == 5 {
+				addItems = append(addItems, "胰腺MRI")
+			} else {
+				addItems = append(addItems, "胰腺CT/MRI")
+			}
+		}
+
+		if survey.CheckAnswerKeysIsValid(*surveys["Symptom"], []string{"DjMbBz"}) { //大便习惯改变(便秘、腹泻等)、大便形状改变、大便性质改变(便血、粘液便等)、腹部固定部位疼痛,且任一症状持续两周以上
+			addItems = append(addItems, "肠镜")
+			addItems = append(addItems, "粪便免疫化学测试")
+		}
+
+		if survey.CheckAnswerKeysIsValid(*surveys["Symptom"], []string{"rZwSN7"}) { //乳房包块并伴乳房胀痛(与月经周期无关)、乳头异常分泌物
+			if userInfoTrans >= 2 {
+				addItems = append(addItems, "乳腺X线(钼靶)")
+			}
+		}
+	}
+
+	//您的生活饮食习惯是?
+	if surveys["EatingHabits"] != nil {
+		if survey.CheckAnswerKeysIsValid(*surveys["EatingHabits"], []string{"4XpSj9"}) { //热烫饮食
+			addItems = append(addItems, "食管内镜")
+		}
+	}
+
+	//检查错误信息
+	if len(errInfo.Text) > 0 {
+		result := SurveyResult{}
+		err := errInfo.Text
+		errInfo.Text = []string{"您的答题结果有误,无法为您推荐体检项目,请重新答题。", "错误信息:" + strings.Join(err, "、")}
+		result.SurveyResultDatas = append(result.SurveyResultDatas, errInfo)
+		return &result
+	}
+
+	for k, v := range sysItemsCopy {
+		if strings.Contains(strings.Join(addItems, ","), v.Name) {
+			sysItemsCopy[k].IsAdd = true
+		}
+	}
+
+	calItems := []Item{}
+
+	femaleInfo := "0"
+	if userInfoTrans >= 2 {
+		femaleInfo = "2"
+	}
+
+	for _, v := range sysItemsCopy {
+		if (strings.Contains(v.Scope, "0") || strings.Contains(v.Scope, userInfo) || strings.Contains(v.Scope, femaleInfo)) && (v.IsBasic || v.IsAdd) {
+			if v.Radioactivity && isPregnancy {
+				continue
+			}
+			calItems = append(calItems, v)
+		}
+	}
+
+	ignoreItems := []string{}
+	finalCalItems := []Item{}
+	for _, v := range calItems {
+		if len(v.IgnoreItem) > 0 {
+			ignoreItems = append(ignoreItems, v.IgnoreItem...)
+		}
+	}
+
+	for _, v := range calItems {
+		if !strings.Contains(strings.Join(ignoreItems, ","), v.Name) {
+			finalCalItems = append(finalCalItems, v)
+		}
+	}
+
+	resultItems := SurveyResultTemplate{
+		Type: "titleAndContents",
+	}
+
+	for _, v := range finalCalItems {
+		singleItems := TitleAndContent{}
+		singleItems.Title = v.Name
+		content := []string{}
+		for _, vv := range v.ChildrenItems {
+			if vv.Name != "" && (vv.IsBasic || v.IsAdd) && (strings.Contains(vv.Scope, "0") || strings.Contains(vv.Scope, userInfo) || strings.Contains(vv.Scope, femaleInfo)) {
+				if vv.Radioactivity && isPregnancy {
+					continue
+				}
+				content = append(content, vv.Name)
+			}
+		}
+		singleItems.Content = strings.Join(content, "、")
+		resultItems.TitleAndContents = append(resultItems.TitleAndContents, singleItems)
+	}
+
+	result.SurveyResultDatas = append(result.SurveyResultDatas, resultItems)
+
+	return &result
+}

+ 749 - 0
sdk/survey_disease/structs.go

@@ -0,0 +1,749 @@
+package survey_disease
+
+type TitleAndContent struct {
+	Title   string `json:"title"`   //标题
+	Content string `json:"content"` //内容
+}
+
+// 结果模板
+type SurveyResultTemplate struct {
+	Type             string            `json:"type"`             //模板类型 1:多行纯文本展示样式(Text) 2:多行标题与内容展示样式(TitleAndContents)
+	Text             []string          `json:"text"`             //文本内容
+	TitleAndContents []TitleAndContent `json:"titleAndContents"` //多标题与内容
+}
+
+// 输出结果
+type SurveyResult struct {
+	SurveyResultDatas []SurveyResultTemplate `json:"surveyResultData"` //结果数据
+	AlTime            string                 `json:"alTime"`           //计算时间
+}
+
+type Item struct {
+	Name          string   `json:"name"`          //项目名称
+	Scope         string   `json:"scope"`         //适用范围 0-不限,1-男性,2-女性 3-女性未婚 4-女性已婚 5-备孕女性
+	Radioactivity bool     `json:"radioactivity"` //是否放射性
+	IsBasic       bool     `json:"isBasic"`       //是否基础项目
+	IgnoreItem    []string `json:"ignoreItem"`    //忽略的项目
+	IsAdd         bool     `json:"isAdd"`         //是否附加项目
+	ChildrenItems []Item   `json:"childrenItems"` //子项目
+}
+
+var SysItemsForDiseaseScreening = []Item{
+	{
+		Name:          "一般检查",
+		Scope:         "0",
+		Radioactivity: false,
+		IsBasic:       true,
+		ChildrenItems: []Item{
+			{
+				Name:          "身高",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "体重",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "腰围",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "臀围",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "血压",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "脉搏",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+		},
+	},
+	{
+		Name:          "内科检查",
+		Scope:         "0",
+		Radioactivity: false,
+		IsBasic:       true,
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "外科检查(含肛门指诊)",
+		Scope:         "0",
+		Radioactivity: false,
+		IsBasic:       true,
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "眼科检查",
+		Scope:         "0",
+		Radioactivity: false,
+		IsBasic:       true,
+		ChildrenItems: []Item{
+			{
+				Name:          "视力",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "眼压",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "眼底检查",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+		},
+	},
+	{
+		Name:          "耳鼻喉科",
+		Scope:         "0",
+		Radioactivity: false,
+		IsBasic:       true,
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "口腔科",
+		Scope:         "0",
+		Radioactivity: false,
+		IsBasic:       true,
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "妇科检查",
+		Scope:         "4",
+		Radioactivity: false,
+		IsBasic:       true,
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "宫颈液基细胞学检查",
+		Scope:         "4",
+		Radioactivity: false,
+		IsBasic:       true,
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "HPV检查",
+		Scope:         "4",
+		Radioactivity: false,
+		IsBasic:       true,
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "血常规",
+		Scope:         "0",
+		Radioactivity: false,
+		IsBasic:       true,
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "尿常规",
+		Scope:         "0",
+		Radioactivity: false,
+		IsBasic:       true,
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "粪便常规",
+		Scope:         "0",
+		Radioactivity: false,
+		IsBasic:       true,
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "粪便隐血",
+		Scope:         "0",
+		Radioactivity: false,
+		IsBasic:       true,
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "粪便免疫化学测试",
+		Scope:         "0",
+		Radioactivity: false,
+		IsBasic:       false,
+		IgnoreItem:    []string{"粪便隐血"},
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "血糖",
+		Scope:         "0",
+		Radioactivity: false,
+		IsBasic:       true,
+		ChildrenItems: []Item{
+			{
+				Name:          "空腹血糖",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "糖化血红蛋白",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+		},
+	},
+	{
+		Name:          "血脂",
+		Scope:         "0",
+		Radioactivity: false,
+		IsBasic:       true,
+		ChildrenItems: []Item{
+			{
+				Name:          "总胆固醇",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "甘油三酯",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "高密度脂蛋白",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "低密度脂蛋白",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+		},
+	},
+	{
+		Name:          "肝功能",
+		Scope:         "0",
+		Radioactivity: false,
+		IsBasic:       true,
+		ChildrenItems: []Item{
+			{
+				Name:          "白蛋白",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "丙氨酸氨基转移酶",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "天门冬氨酸氨基转移酶",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "总胆红素",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+		},
+	},
+	{
+		Name:          "肾功能",
+		Scope:         "0",
+		Radioactivity: false,
+		IsBasic:       true,
+		ChildrenItems: []Item{
+			{
+				Name:          "血尿素氮",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "血肌酐",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "血尿酸",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+		},
+	},
+	{
+		Name:          "甲状腺功能七项",
+		Scope:         "0",
+		Radioactivity: false,
+		IsBasic:       true,
+		ChildrenItems: []Item{
+			{
+				Name:          "三碘甲腺原氨酸",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "甲状腺素",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "游离三碘甲状腺原氨酸",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "游离甲状腺素",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "促甲状腺激素",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "甲状腺球蛋白抗体",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "甲状腺过氧化物酶抗体",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+		},
+	},
+	{
+		Name:          "乙肝两对半",
+		Scope:         "0",
+		Radioactivity: false,
+		IsBasic:       true,
+		ChildrenItems: []Item{
+			{
+				Name:          "乙肝表面抗原",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "乙肝表面抗体",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "乙肝e抗原",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "乙肝e抗体",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "乙肝核心抗体",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+		},
+	},
+	{
+		Name:          "乙肝病毒DNA定量",
+		Scope:         "0",
+		Radioactivity: false,
+		IsBasic:       false,
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "幽门螺杆菌检测",
+		Scope:         "1",
+		Radioactivity: true,
+		IsBasic:       true,
+		IgnoreItem:    []string{},
+		ChildrenItems: []Item{
+			{
+				Name:          "C13呼气试验/C14呼气试验",
+				Scope:         "1",
+				Radioactivity: true,
+				IsBasic:       true,
+			},
+		},
+	},
+	{
+		Name:          "幽门螺杆菌检测",
+		Scope:         "2",
+		Radioactivity: true,
+		IsBasic:       true,
+		IgnoreItem:    []string{"幽门螺杆菌检查"},
+		ChildrenItems: []Item{
+			{
+				Name:          "C13呼气试验/C14呼气试验",
+				Scope:         "2",
+				Radioactivity: true,
+				IsBasic:       true,
+			},
+		},
+	},
+	{
+		Name:          "幽门螺杆菌检查",
+		Scope:         "2",
+		Radioactivity: false,
+		IsBasic:       true,
+		IgnoreItem:    []string{},
+		ChildrenItems: []Item{
+			{
+				Name:          "C13呼气试验",
+				Scope:         "2",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+		},
+	},
+
+	{
+		Name:          "胃功能",
+		Scope:         "0",
+		Radioactivity: false,
+		IsBasic:       false,
+		ChildrenItems: []Item{
+			{
+				Name:          "胃蛋白酶原I",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       false,
+			},
+			{
+				Name:          "胃蛋白酶原II",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       false,
+			},
+			{
+				Name:          "胃蛋白酶原比值",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       false,
+			},
+			{
+				Name:          "胃泌素17",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       false,
+			},
+		},
+	},
+
+	{
+		Name:          "肿瘤标志物",
+		Scope:         "0",
+		Radioactivity: false,
+		IsBasic:       true,
+		ChildrenItems: []Item{
+			{
+				Name:          "甲胎蛋白测定(AFP)定量",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "EB病毒衣壳抗原IgA抗体(VCA-IgA)",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "癌胚抗原测定(CEA)定量",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "神经元特异烯醇化酶测定(NSE)",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "细胞角蛋白19片段测定(CYFRA21-1)",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "糖链抗原50测定(CA50)",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "糖链抗原19-9测定(CA19-9)",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "糖链抗原242测定(CA242)",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "糖链抗原72-4测定(CA72-4)",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "特异生长因子测定(TSGF)",
+				Scope:         "0",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "总前列腺特异性抗原测定(TPSA)",
+				Scope:         "1",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "游离前列腺特异性抗原测定(FPSA)",
+				Scope:         "1",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "糖链抗原125测定(CA125)",
+				Scope:         "2",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+			{
+				Name:          "糖链抗原15-3测定(CA15-3)",
+				Scope:         "2",
+				Radioactivity: false,
+				IsBasic:       true,
+			},
+		},
+	},
+	{
+		Name:          "肠癌基因检测",
+		Scope:         "0",
+		Radioactivity: false,
+		IsBasic:       false,
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "BRCA基因检测",
+		Scope:         "0",
+		Radioactivity: false,
+		IsBasic:       false,
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "十二导联心电图",
+		Scope:         "0",
+		Radioactivity: false,
+		IsBasic:       true,
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "心脏彩超",
+		Scope:         "0",
+		Radioactivity: false,
+		IsBasic:       true,
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "颈动脉彩超",
+		Scope:         "0",
+		Radioactivity: false,
+		IsBasic:       true,
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "甲状腺彩超",
+		Scope:         "0",
+		Radioactivity: false,
+		IsBasic:       true,
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "肝脏彩超",
+		Scope:         "0",
+		Radioactivity: false,
+		IsBasic:       true,
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "胆囊彩超",
+		Scope:         "0",
+		Radioactivity: false,
+		IsBasic:       true,
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "胰腺彩超",
+		Scope:         "0",
+		Radioactivity: false,
+		IsBasic:       true,
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "胰腺CT",
+		Scope:         "0",
+		Radioactivity: true,
+		IsBasic:       false,
+		IgnoreItem:    []string{"胰腺彩超"},
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "胰腺MRI",
+		Scope:         "0",
+		Radioactivity: false,
+		IsBasic:       false,
+		IgnoreItem:    []string{"胰腺彩超"},
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "胰腺CT/MRI",
+		Scope:         "0",
+		Radioactivity: true,
+		IsBasic:       false,
+		IgnoreItem:    []string{"胰腺CT", "胰腺MRI", "胰腺彩超"},
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "脾脏彩超",
+		Scope:         "0",
+		Radioactivity: false,
+		IsBasic:       true,
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "肾脏彩超",
+		Scope:         "0",
+		Radioactivity: false,
+		IsBasic:       true,
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "膀胱彩超",
+		Scope:         "0",
+		Radioactivity: false,
+		IsBasic:       true,
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "输尿管彩超",
+		Scope:         "0",
+		Radioactivity: false,
+		IsBasic:       true,
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "前列腺彩超",
+		Scope:         "1",
+		Radioactivity: false,
+		IsBasic:       true,
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "乳腺彩超",
+		Scope:         "2",
+		Radioactivity: false,
+		IsBasic:       true,
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "妇科彩超(经腹)",
+		Scope:         "3",
+		Radioactivity: false,
+		IsBasic:       true,
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "妇科彩超(经阴道)",
+		Scope:         "4",
+		Radioactivity: false,
+		IsBasic:       true,
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "乳腺X线(钼靶)",
+		Scope:         "2",
+		Radioactivity: true,
+		IsBasic:       false,
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "胸部低剂量螺旋CT",
+		Scope:         "0",
+		Radioactivity: true,
+		IsBasic:       true,
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "骨密度",
+		Scope:         "0",
+		Radioactivity: true,
+		IsBasic:       true,
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "食管内镜",
+		Scope:         "0",
+		Radioactivity: false,
+		IsBasic:       false,
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "胃镜",
+		Scope:         "0",
+		Radioactivity: false,
+		IsBasic:       false,
+		IgnoreItem:    []string{"食管内镜"},
+		ChildrenItems: []Item{},
+	},
+	{
+		Name:          "肠镜",
+		Scope:         "0",
+		Radioactivity: false,
+		IsBasic:       false,
+		ChildrenItems: []Item{},
+	},
+}

BIN
service/.DS_Store


+ 192 - 0
service/questionnaire_subject/questionnaire_subject.go

@@ -0,0 +1,192 @@
+package questionnaire_subject
+
+import (
+	"surveyService/model"
+	"surveyService/response"
+	"surveyService/validators"
+
+	"github.com/golang-module/carbon"
+)
+
+// 创建
+func Create(subject *validators.QuestionnaireSubject) *response.ErrCode {
+	// 检查编号是否重复
+	existsErr := ExistBySn(subject.SN, 0)
+	if existsErr != nil {
+		return existsErr
+	}
+
+	// 创建
+	subjectModel := model.QuestionnaireSubject{
+		SN:        subject.SN,
+		Type:      subject.Type,
+		Title:     subject.Title,
+		Validator: subject.Validator,
+		Remark:    subject.Remark,
+		Mark:      subject.Mark,
+	}
+	if err := model.DB.Create(&subjectModel).Error; err != nil {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "创建失败",
+		}
+	}
+
+	return nil
+}
+
+// 更新
+func Update(subject *validators.QuestionnaireSubject) *response.ErrCode {
+	if subject.ID <= 0 {
+		return &response.ErrCode{
+			Code: response.VALIDATOR_ERROR,
+			Msg:  "ID不能为空",
+		}
+	}
+	// 检查编号是否重复
+	existsErr := ExistBySn(subject.SN, subject.ID)
+	if existsErr != nil {
+		return existsErr
+	}
+
+	// 更新
+	model.DB.Where("id = ?", subject.ID).Select("type", "title", "remark", "validator").Updates(model.QuestionnaireSubject{
+		Type:      subject.Type,
+		Title:     subject.Title,
+		Validator: subject.Validator,
+		Remark:    subject.Remark,
+	})
+
+	return nil
+}
+
+// 获取单个
+func Find(id int64) (*model.QuestionnaireSubject, *response.ErrCode) {
+	var subject model.QuestionnaireSubject
+	model.DB.Where("id = ?", id).First(&subject)
+	if subject.ID <= 0 {
+		return nil, &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "没有找到有效的数据",
+		}
+	}
+	return &subject, nil
+}
+
+// 删除
+func Delete(id int64) *response.ErrCode {
+	// 检查问题库是否被问题模板关联
+	var total int64
+	model.DB.Model(&model.QuestionnaireTemplateSubject{}).Where("subject_id = ?", id).Count(&total)
+	if total > 0 {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "该题目已被问题模板关联,不能删除",
+		}
+	}
+
+	// 检查问题库是否被问卷关联
+	model.DB.Model(&model.QuestionnaireSubject{}).Where("subject_id = ?", id).Count(&total)
+	if total > 0 {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "该题目已被问卷关联,不能删除",
+		}
+	}
+
+	// 执行删除
+	model.DB.Where("id = ?", id).Delete(&model.QuestionnaireSubject{})
+
+	return nil
+}
+
+// 修改备注
+func UpdateMark(id int64, mark string) *response.ErrCode {
+	_, findErr := Find(id)
+	if findErr != nil {
+		return findErr
+	}
+
+	// 更新
+	model.DB.Where("id = ?", id).Select("mark").Updates(model.QuestionnaireSubject{
+		Mark: mark,
+	})
+
+	return nil
+}
+
+func ExistBySn(sn string, subjectId int64) *response.ErrCode {
+	// 检查编号是否重复
+	query := model.DB.Where("sn = ?", sn)
+	if subjectId > 0 {
+		query = query.Where("id <> ?", subjectId)
+	}
+	var total int64
+	query.Model(&model.QuestionnaireSubject{}).Count(&total)
+
+	if total > 0 {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "编号已存在",
+		}
+	}
+
+	return nil
+}
+
+// 获取列表
+func Paginate(page, pageSize int, key string) ([]*model.QuestionnaireSubject, int64) {
+	var total int64
+	var subjects []*model.QuestionnaireSubject = make([]*model.QuestionnaireSubject, 0)
+
+	query := model.DB.Model(&model.QuestionnaireSubject{})
+	if key != "" {
+		query = query.Where("title like @key or sn like @key", map[string]interface{}{
+			"key": "%" + key + "%",
+		})
+	}
+	query.Count(&total)
+
+	if total > 0 {
+		query.Scopes(model.Paginate(page, pageSize)).Find(&subjects)
+	}
+
+	return subjects, total
+}
+
+// 获取所有列表
+func List(key string) []*model.QuestionnaireSubject {
+	var subjects []*model.QuestionnaireSubject = make([]*model.QuestionnaireSubject, 0)
+	query := model.DB.Model(&model.QuestionnaireSubject{})
+	if key != "" {
+		query = query.Where("title like @key or sn like @key", map[string]interface{}{
+			"key": "%" + key + "%",
+		})
+	}
+	query.Find(&subjects)
+	return subjects
+}
+
+// 通过ID获取列表
+func ListByIds(ids []int64) []*model.QuestionnaireSubject {
+	var subjects []*model.QuestionnaireSubject = make([]*model.QuestionnaireSubject, 0)
+	model.DB.Where("id in (?)", ids).Find(&subjects)
+	return subjects
+}
+
+// 格式化
+func Format(subject *model.QuestionnaireSubject) *validators.QuestionnaireSubject {
+	if subject == nil {
+		return nil
+	}
+	return &validators.QuestionnaireSubject{
+		ID:        subject.ID,
+		SN:        subject.SN,
+		Type:      subject.Type,
+		Title:     subject.Title,
+		Validator: subject.Validator,
+		Remark:    subject.Remark,
+		Mark:      subject.Mark,
+		CreatedAt: carbon.Time2Carbon(subject.CreatedAt).Format("Y/m/d H:i:s"),
+	}
+}

+ 257 - 0
service/questionnaire_survey/survey.go

@@ -0,0 +1,257 @@
+package questionnaire_survey
+
+import (
+	"surveyService/model"
+	"surveyService/response"
+	"surveyService/validators"
+
+	"github.com/golang-module/carbon"
+	"gorm.io/gorm"
+)
+
+// 创建
+func Create(survey *validators.QuestionnaireSurvey) *response.ErrCode {
+
+	// 检查编号是否存在
+	if survey.SN == "" {
+		return &response.ErrCode{
+			Code: response.VALIDATOR_ERROR,
+			Msg:  "编号不能为空",
+		}
+	}
+
+	// 检查编号是否重复
+	existsErr := ExistBySn(survey.SN, 0)
+	if existsErr != nil {
+		return existsErr
+	}
+
+	// 创建
+	templateModel := model.QuestionnaireSurvey{
+		SN:             survey.SN,
+		Title:          survey.Title,
+		CanSkipIntro:   survey.CanSkipIntro,
+		CanSkipResult:  survey.CanSkipResult,
+		GuestAvatar:    survey.GuestAvatar,
+		CustomerAvatar: survey.CustomerAvatar,
+		GreetingText:   survey.GreetingText,
+		FinishedText:   survey.FinishedText,
+		Intro:          survey.Intro,
+		Status:         model.QUESTIONNAIRE_SURVEY_STATUS_ENABLE,
+		Type:           model.QUESTIONNAIRE_SURVEY_TYPE_NORMAL,
+	}
+	if err := model.DB.Create(&templateModel).Error; err != nil {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "创建失败",
+		}
+	}
+
+	return nil
+}
+
+// 更新
+func Update(survey *validators.QuestionnaireSurvey) *response.ErrCode {
+	if survey.ID <= 0 {
+		return &response.ErrCode{
+			Code: response.VALIDATOR_ERROR,
+			Msg:  "ID不能为空",
+		}
+	}
+	// 检查编号是否重复
+	existsErr := ExistBySn(survey.SN, survey.ID)
+	if existsErr != nil {
+		return existsErr
+	}
+
+	updateErr := model.DB.Transaction(func(tx *gorm.DB) error {
+		// 更新
+		tx.Model(&model.QuestionnaireSurvey{}).Where("id = ?", survey.ID).Select([]string{"SN", "Title", "CanSkipIntro", "CanSkipResult", "GuestAvatar", "CustomerAvatar", "GreetingText", "FinishedText", "Intro"}).Updates(model.QuestionnaireSurvey{
+			SN:             survey.SN,
+			Title:          survey.Title,
+			CanSkipIntro:   survey.CanSkipIntro,
+			CanSkipResult:  survey.CanSkipResult,
+			GuestAvatar:    survey.GuestAvatar,
+			CustomerAvatar: survey.CustomerAvatar,
+			GreetingText:   survey.GreetingText,
+			FinishedText:   survey.FinishedText,
+			Intro:          survey.Intro,
+		})
+		return nil
+	})
+
+	if updateErr != nil {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  updateErr.Error(),
+		}
+	}
+
+	return nil
+}
+
+// 获取单个
+func Find(id int64) (*model.QuestionnaireSurvey, *response.ErrCode) {
+	var survey model.QuestionnaireSurvey
+	model.DB.Where("id = ?", id).Preload("Subjects", func(db *gorm.DB) *gorm.DB {
+		return db.Order("sort ASC")
+	}).Preload("Subjects.Subject").First(&survey)
+	if survey.ID <= 0 {
+		return nil, &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "没有找到有效的问卷",
+		}
+	}
+	return &survey, nil
+}
+
+// 获取单个
+func FindBySN(sn string) (*model.QuestionnaireSurvey, *response.ErrCode) {
+	var survey model.QuestionnaireSurvey
+	model.DB.Where("sn = ?", sn).Preload("Subjects", func(db *gorm.DB) *gorm.DB {
+		return db.Order("sort ASC")
+	}).Preload("Subjects.Subject").First(&survey)
+	if survey.ID <= 0 {
+		return nil, &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "没有找到有效的问卷",
+		}
+	}
+	return &survey, nil
+}
+
+// 删除
+func Delete(id int64) *response.ErrCode {
+	// 删除问卷
+	model.DB.Where("id = ?", id).Delete(&model.QuestionnaireSurvey{})
+	// 删除问卷关联的数据
+	model.DB.Where("survey_id = ?", id).Delete(&model.QuestionnaireSurveyQuestionnaireSubject{})
+	// Todo 决策模型关联删除
+	return nil
+}
+
+// 检查编号是否已存在
+func ExistBySn(sn string, subjectId int64) *response.ErrCode {
+	// 检查编号是否重复
+	query := model.DB.Where("sn = ?", sn)
+	if subjectId > 0 {
+		query = query.Where("id <> ?", subjectId)
+	}
+	var total int64
+	query.Model(&model.QuestionnaireSurvey{}).Count(&total)
+
+	if total > 0 {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "编号已存在",
+		}
+	}
+
+	return nil
+}
+
+// 获取列表(分页)
+func Paginate(page, pageSize int, key string) ([]*model.QuestionnaireSurvey, int64) {
+	var total int64
+	var subjects []*model.QuestionnaireSurvey = make([]*model.QuestionnaireSurvey, 0)
+
+	query := model.DB.Model(&model.QuestionnaireSurvey{})
+	if key != "" {
+		query = query.Where("title like @key or sn like @key", map[string]interface{}{
+			"key": "%" + key + "%",
+		})
+	}
+	query.Count(&total)
+
+	if total > 0 {
+		query.Scopes(model.Paginate(page, pageSize)).Preload("SystemTags").Find(&subjects)
+	}
+
+	return subjects, total
+}
+
+// 获取所有列表
+func List(key string) []*model.QuestionnaireSurvey {
+	var subjects []*model.QuestionnaireSurvey = make([]*model.QuestionnaireSurvey, 0)
+
+	query := model.DB.Model(&model.QuestionnaireSurvey{})
+	if key != "" {
+		query = query.Where("title like @key or sn like @key", map[string]interface{}{
+			"key": "%" + key + "%",
+		})
+	}
+	query.Preload("SystemTags").Find(&subjects)
+	return subjects
+}
+
+// 修改Peg.js
+func UpdatePeg(id int64, peg string) *response.ErrCode {
+	_, findErr := Find(id)
+	if findErr != nil {
+		return findErr
+	}
+
+	// 更新
+	model.DB.Model(model.QuestionnaireSurvey{}).Where("id = ?", id).Update("peg", peg)
+
+	return nil
+}
+
+// 修改备注
+func UpdateRemark(id int64, remark string) *response.ErrCode {
+	_, findErr := Find(id)
+	if findErr != nil {
+		return findErr
+	}
+
+	// 更新
+	model.DB.Model(model.QuestionnaireSurvey{}).Where("id = ?", id).Update("remark", remark)
+
+	return nil
+}
+
+// 修改状态
+func UpdateStatus(id int64, status int) *response.ErrCode {
+	_, findErr := Find(id)
+	if findErr != nil {
+		return findErr
+	}
+
+	// 更新
+	model.DB.Model(model.QuestionnaireSurvey{}).Where("id = ?", id).Update("status", status)
+
+	return nil
+}
+
+// 格式化
+func Format(survey *model.QuestionnaireSurvey) *validators.QuestionnaireSurvey {
+	if survey == nil {
+		return nil
+	}
+	var subjects = make([]*validators.SurveyQuestionnaireSubject, 0)
+	for _, subject := range survey.Subjects {
+		subjects = append(subjects, FormatSurveySubject(subject))
+	}
+
+	return &validators.QuestionnaireSurvey{
+		ID:             survey.ID,
+		SN:             survey.SN,
+		Title:          survey.Title,
+		CanSkipIntro:   survey.CanSkipIntro,
+		CanSkipResult:  survey.CanSkipResult,
+		GuestAvatar:    survey.GuestAvatar,
+		CustomerAvatar: survey.CustomerAvatar,
+		GreetingText:   survey.GreetingText,
+		FinishedText:   survey.FinishedText,
+		Intro:          survey.Intro,
+		Status:         survey.Status,
+		Type:           survey.Type,
+		Returns:        survey.Returns,
+		Dsl:            survey.Dsl,
+		Peg:            survey.Peg,
+		Remark:         survey.Remark,
+		Subjects:       subjects,
+		CreatedAt:      carbon.Time2Carbon(survey.CreatedAt).Format("Y/m/d H:i:s"),
+		UpdatedAt:      carbon.Time2Carbon(survey.UpdatedAt).Format("Y/m/d H:i:s"),
+	}
+}

+ 112 - 0
service/questionnaire_survey/survey_subject.go

@@ -0,0 +1,112 @@
+package questionnaire_survey
+
+import (
+	"fmt"
+	"surveyService/model"
+	"surveyService/response"
+	"surveyService/service/questionnaire_subject"
+	"surveyService/validators"
+
+	"github.com/samber/lo"
+	"gorm.io/gorm"
+)
+
+// 获取问卷下的问题库列表
+func ListSubjects(surveyId int64) []*model.QuestionnaireSurveyQuestionnaireSubject {
+	var subjects []*model.QuestionnaireSurveyQuestionnaireSubject
+
+	// 获取问卷下的问题库列表
+	model.DB.Where("survey_id = ?", surveyId).Preload("Subject").Order("sort asc").Order("id asc").Find(&subjects)
+
+	return subjects
+}
+
+// 删除问卷下的问题库
+func DeleteSubject(surveyId int64, subjectIds []int64) *response.ErrCode {
+	model.DB.Where("survey_id = ?", surveyId).Where("id in (?)", subjectIds).Delete(&model.QuestionnaireSurveyQuestionnaireSubject{})
+
+	return nil
+}
+
+// 添加问卷下的问题库
+func AddSubject(surveyId int64, subjectIds []int64) *response.ErrCode {
+	// 检查模板是否存在
+	_, findErr := Find(surveyId)
+	if findErr != nil {
+		return findErr
+	}
+
+	if len(subjectIds) > 0 {
+		// 检查问题是否都存在
+		existsSubjects := questionnaire_subject.ListByIds(subjectIds)
+		if len(existsSubjects) < len(subjectIds) {
+			return &response.ErrCode{
+				Code: response.ERROR,
+				Msg:  fmt.Sprintf("有%d个问题无效或不存在", len(subjectIds)-len(existsSubjects)),
+			}
+		}
+
+		// 将已经存在的问卷下的问题过滤掉
+		var existsSubjectIds []int64
+		model.DB.Model(&model.QuestionnaireSurveyQuestionnaireSubject{}).Where("survey_id = ?", surveyId).Where("subject_id in (?)", subjectIds).Pluck("subject_id", &existsSubjectIds)
+
+		// 取差集
+		diffSubjectIds, _ := lo.Difference(subjectIds, existsSubjectIds)
+
+		if len(diffSubjectIds) > 0 {
+			var surveySubjects []*model.QuestionnaireSurveyQuestionnaireSubject
+			for _, subjectId := range diffSubjectIds {
+				surveySubjects = append(surveySubjects, &model.QuestionnaireSurveyQuestionnaireSubject{
+					SurveyID:  surveyId,
+					SubjectID: subjectId,
+					Sort:      999,
+				})
+			}
+
+			// 批量插入
+			model.DB.CreateInBatches(&surveySubjects, 100)
+		}
+	}
+	return nil
+}
+
+// 批量修改排序
+func UpdateSort(surveyId int64, surveySubjectIds []int64) *response.ErrCode {
+	// 检查模板是否存在
+	_, findErr := Find(surveyId)
+	if findErr != nil {
+		return findErr
+	}
+
+	if len(surveySubjectIds) > 0 {
+		sqlExpr := "case id"
+		for surveySubjectIndex, surveySubjectId := range surveySubjectIds {
+			sqlExpr += " when " + fmt.Sprint(surveySubjectId) + " then " + fmt.Sprint(surveySubjectIndex+1)
+		}
+		sqlExpr += " else sort end"
+		model.DB.Model(&model.QuestionnaireSurveyQuestionnaireSubject{}).Where("survey_id = ?", surveyId).Update("sort", gorm.Expr(sqlExpr))
+	}
+
+	return nil
+}
+
+// 修改是否必填
+func UpdateIsRequired(id int64, isRequired bool) *response.ErrCode {
+
+	model.DB.Model(&model.QuestionnaireSurveyQuestionnaireSubject{}).Where("id = ?", id).Update("is_required", isRequired)
+
+	return nil
+}
+
+// 格式化
+func FormatSurveySubject(surveySubject *model.QuestionnaireSurveyQuestionnaireSubject) *validators.SurveyQuestionnaireSubject {
+	if surveySubject == nil {
+		return nil
+	}
+	return &validators.SurveyQuestionnaireSubject{
+		ID:                   surveySubject.ID,
+		Sort:                 surveySubject.Sort,
+		IsRequired:           surveySubject.IsRequired == 1,
+		QuestionnaireSubject: questionnaire_subject.Format(&surveySubject.Subject),
+	}
+}

+ 194 - 0
service/questionnaire_template/questionnaire_template.go

@@ -0,0 +1,194 @@
+package questionnaire_template
+
+import (
+	"surveyService/model"
+	"surveyService/response"
+	"surveyService/service/questionnaire_subject"
+	"surveyService/validators"
+
+	"github.com/golang-module/carbon"
+	"gorm.io/gorm"
+)
+
+// 创建
+func Create(template *validators.QuestionnaireTemplate) *response.ErrCode {
+
+	// 检查编号是否存在
+	if template.SN == "" {
+		return &response.ErrCode{
+			Code: response.VALIDATOR_ERROR,
+			Msg:  "编号不能为空",
+		}
+	}
+
+	// 检查编号是否重复
+	existsErr := ExistBySn(template.SN, 0)
+	if existsErr != nil {
+		return existsErr
+	}
+
+	// 创建
+	templateModel := model.QuestionnaireTemplate{
+		SN:    template.SN,
+		Title: template.Title,
+	}
+	if err := model.DB.Create(&templateModel).Error; err != nil {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "创建失败",
+		}
+	}
+
+	return nil
+}
+
+// 更新
+func Update(template *validators.QuestionnaireTemplate) *response.ErrCode {
+	if template.ID <= 0 {
+		return &response.ErrCode{
+			Code: response.VALIDATOR_ERROR,
+			Msg:  "ID不能为空",
+		}
+	}
+	// 检查编号是否重复
+	existsErr := ExistBySn(template.SN, template.ID)
+	if existsErr != nil {
+		return existsErr
+	}
+
+	updateErr := model.DB.Transaction(func(tx *gorm.DB) error {
+		// 更新
+		tx.Where("id = ?", template.ID).Select("title").Updates(model.QuestionnaireSubject{
+			Title: template.Title,
+		})
+
+		return nil
+	})
+
+	if updateErr != nil {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  updateErr.Error(),
+		}
+	}
+
+	return nil
+}
+
+// 获取单个
+func Find(id int64) (*model.QuestionnaireTemplate, *response.ErrCode) {
+	var template model.QuestionnaireTemplate
+	model.DB.Where("id = ?", id).First(&template)
+	if template.ID <= 0 {
+		return nil, &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "没有找到有效的问题模板",
+		}
+	}
+	return &template, nil
+}
+
+// 删除
+func Delete(id int64) *response.ErrCode {
+	// 删除问题模板
+	model.DB.Where("id = ?", id).Delete(&model.QuestionnaireTemplate{})
+	// 删除问题模板关联的数据
+	model.DB.Where("template_id = ?", id).Delete(&model.QuestionnaireTemplateSubject{})
+
+	return nil
+}
+
+// 检查编号是否已存在
+func ExistBySn(sn string, subjectId int64) *response.ErrCode {
+	// 检查编号是否重复
+	query := model.DB.Where("sn = ?", sn)
+	if subjectId > 0 {
+		query = query.Where("id <> ?", subjectId)
+	}
+	var total int64
+	query.Model(&model.QuestionnaireTemplate{}).Count(&total)
+
+	if total > 0 {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "编号已存在",
+		}
+	}
+
+	return nil
+}
+
+// 获取列表(分页)
+func Paginate(page, pageSize int, key string) ([]*model.QuestionnaireTemplate, int64) {
+	var total int64
+	var subjects []*model.QuestionnaireTemplate = make([]*model.QuestionnaireTemplate, 0)
+
+	query := model.DB.Model(&model.QuestionnaireTemplate{})
+	if key != "" {
+		query = query.Where("title like @key or sn like @key", map[string]interface{}{
+			"key": "%" + key + "%",
+		})
+	}
+	query.Count(&total)
+
+	if total > 0 {
+		query.Scopes(model.Paginate(page, pageSize)).Preload("SubjectRelations.Subject").Preload("SystemTags").Find(&subjects)
+	}
+
+	return subjects, total
+}
+
+// 获取所有列表
+func List(key string) []*model.QuestionnaireTemplate {
+	var subjects []*model.QuestionnaireTemplate = make([]*model.QuestionnaireTemplate, 0)
+
+	query := model.DB.Model(&model.QuestionnaireTemplate{})
+	if key != "" {
+		query = query.Where("title like @key or sn like @key", map[string]interface{}{
+			"key": "%" + key + "%",
+		})
+	}
+	query.Preload("SubjectRelations.Subject").Preload("SystemTags").Find(&subjects)
+	return subjects
+}
+
+// 修改Peg.js
+func UpdatePeg(id int64, peg string) *response.ErrCode {
+	_, findErr := Find(id)
+	if findErr != nil {
+		return findErr
+	}
+
+	// 更新
+	err := model.DB.Model(model.QuestionnaireTemplate{}).Where("id = ?", id).Update("peg", peg).Error
+	if err != nil {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "更新失败",
+		}
+	}
+
+	return nil
+}
+
+// 格式化
+func Format(template *model.QuestionnaireTemplate) *validators.QuestionnaireTemplate {
+	var subjectTotal int = len(template.SubjectRelations)
+	var subjectIds []string = make([]string, 0)
+	var subjects []*validators.QuestionnaireSubject = make([]*validators.QuestionnaireSubject, 0)
+	for _, subject := range template.SubjectRelations {
+		subjectIds = append(subjectIds, subject.Subject.SN)
+		subjects = append(subjects, questionnaire_subject.Format(&subject.Subject))
+	}
+
+	return &validators.QuestionnaireTemplate{
+		ID:           template.ID,
+		SN:           template.SN,
+		Title:        template.Title,
+		Peg:          template.Peg,
+		SubjectTotal: subjectTotal,
+		SubjectIds:   subjectIds,
+		Subjects:     subjects,
+		CreatedAt:    carbon.Time2Carbon(template.CreatedAt).Format("Y/m/d H:i:s"),
+	}
+}

+ 100 - 0
service/questionnaire_template/questionnaire_template_subject.go

@@ -0,0 +1,100 @@
+package questionnaire_template
+
+import (
+	"fmt"
+	"surveyService/model"
+	"surveyService/response"
+	"surveyService/service/questionnaire_subject"
+	"surveyService/validators"
+
+	"github.com/samber/lo"
+	"gorm.io/gorm"
+)
+
+// 获取问题模板下的问题库列表
+func ListSubjects(templateId int64) []*model.QuestionnaireTemplateSubject {
+	var subjects []*model.QuestionnaireTemplateSubject
+
+	// 获取问题模板下的问题库列表
+	model.DB.Where("template_id = ?", templateId).Preload("Subject").Order("sort asc").Order("id asc").Find(&subjects)
+
+	return subjects
+}
+
+// 删除问题模板下的问题库
+func DeleteSubject(templateId int64, subjectIds []int64) *response.ErrCode {
+	model.DB.Where("template_id = ?", templateId).Where("id in (?)", subjectIds).Delete(&model.QuestionnaireTemplateSubject{})
+
+	return nil
+}
+
+// 添加问题模板下的问题库
+func AddSubject(templateId int64, subjectIds []int64) *response.ErrCode {
+	// 检查模板是否存在
+	_, findErr := Find(templateId)
+	if findErr != nil {
+		return findErr
+	}
+
+	if len(subjectIds) > 0 {
+		// 检查问题是否都存在
+		existsSubjects := questionnaire_subject.ListByIds(subjectIds)
+		if len(existsSubjects) < len(subjectIds) {
+			return &response.ErrCode{
+				Code: response.ERROR,
+				Msg:  fmt.Sprintf("有%d个问题无效或不存在", len(subjectIds)-len(existsSubjects)),
+			}
+		}
+
+		// 将已经存在的问题模板下的问题过滤掉
+		var existsSubjectIds []int64
+		model.DB.Model(&model.QuestionnaireTemplateSubject{}).Where("template_id = ?", templateId).Where("subject_id in (?)", subjectIds).Pluck("subject_id", &existsSubjectIds)
+
+		// 取差集
+		diffSubjectIds, _ := lo.Difference(subjectIds, existsSubjectIds)
+
+		if len(diffSubjectIds) > 0 {
+			var templateSubjects []*model.QuestionnaireTemplateSubject
+			for _, subjectId := range diffSubjectIds {
+				templateSubjects = append(templateSubjects, &model.QuestionnaireTemplateSubject{
+					TemplateID: templateId,
+					SubjectID:  subjectId,
+					Sort:       999,
+				})
+			}
+
+			// 批量插入
+			model.DB.CreateInBatches(&templateSubjects, 100)
+		}
+	}
+	return nil
+}
+
+// 批量修改排序
+func UpdateSort(templateId int64, templateSubjectIds []int64) *response.ErrCode {
+	// 检查模板是否存在
+	_, findErr := Find(templateId)
+	if findErr != nil {
+		return findErr
+	}
+
+	if len(templateSubjectIds) > 0 {
+		sqlExpr := "case id"
+		for templateSubjectIndex, templateSubjectId := range templateSubjectIds {
+			sqlExpr += " when " + fmt.Sprint(templateSubjectId) + " then " + fmt.Sprint(templateSubjectIndex+1)
+		}
+		sqlExpr += " else sort end"
+		model.DB.Model(&model.QuestionnaireTemplateSubject{}).Where("template_id = ?", templateId).Update("sort", gorm.Expr(sqlExpr))
+	}
+
+	return nil
+}
+
+// 格式化
+func FormatTemplateSubject(templateSubject *model.QuestionnaireTemplateSubject) *validators.QuestionnaireTemplateSubject {
+	return &validators.QuestionnaireTemplateSubject{
+		ID:                   templateSubject.ID,
+		Sort:                 templateSubject.Sort,
+		QuestionnaireSubject: questionnaire_subject.Format(&templateSubject.Subject),
+	}
+}

+ 1 - 0
service/structs.go

@@ -0,0 +1 @@
+package service

+ 243 - 0
service/survey/survey.go

@@ -0,0 +1,243 @@
+package survey
+
+import (
+	"surveyService/model"
+	"surveyService/response"
+	"surveyService/validators"
+
+	"github.com/golang-module/carbon"
+)
+
+// 创建或修改问卷
+func UpdateOrCreate(surveyForm *validators.Survey) *response.ErrCode {
+	if surveyForm.ID == "" {
+		return Create(surveyForm)
+	} else {
+		return Update(surveyForm)
+	}
+}
+
+// 创建问卷
+func Create(surveyForm *validators.Survey) *response.ErrCode {
+
+	// // 如果是定制体检,要禁止他创建多个
+	// if surveyForm.Type == model.SURVEY_TYPE_PHYSICAL {
+	// 	// 检查是否有定制体检
+	// 	var physicalSurveyTotal int64
+	// 	model.DB.Model(&model.Survey{}).Where("type = ?", model.SURVEY_TYPE_PHYSICAL).Count(&physicalSurveyTotal)
+	// 	if physicalSurveyTotal > 0 {
+	// 		return &response.ErrCode{
+	// 			Code: response.ERROR,
+	// 			Msg:  "当前已设置有定制体检,请先修改原问卷的类型后再添加",
+	// 		}
+	// 	}
+	// }
+
+	surveyModel := &model.Survey{
+		Name:       surveyForm.Name,
+		Cover:      surveyForm.Cover,
+		SurveyCode: surveyForm.SurveyCode,
+		Type:       surveyForm.Type,
+		Status:     model.SURVEY_STATUS_ENABLE,
+	}
+
+	err := model.DB.Create(surveyModel).Error
+	if err != nil {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "创建问卷失败",
+		}
+	}
+
+	return nil
+}
+
+// 修改问卷
+func Update(surveyForm *validators.Survey) *response.ErrCode {
+	if surveyForm.ID == "" {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "问卷编号不能为空",
+		}
+	}
+	// 获取问卷信息
+	surveyModel, errCode := FindBySn(surveyForm.ID)
+	if errCode != nil {
+		return errCode
+	}
+
+	// 如果没有SurveyCode,表示是默认值,不允许修改类型、模型编号
+	if surveyModel.SurveyCode == "" {
+		if surveyForm.Type != surveyModel.Type {
+			return &response.ErrCode{
+				Code: response.ERROR,
+				Msg:  "问卷类型不允许修改",
+			}
+		}
+		if surveyForm.SurveyCode != "" {
+			return &response.ErrCode{
+				Code: response.ERROR,
+				Msg:  "问卷模型编号不允许修改",
+			}
+		}
+	}
+
+	// // 如果是定制体检,要禁止他创建多个
+	// if surveyForm.Type == model.SURVEY_TYPE_PHYSICAL {
+	// 	// 检查是否有定制体检
+	// 	var physicalSurveyTotal int64
+	// 	model.DB.Model(&model.Survey{}).Where("type = ? and sn != ?", model.SURVEY_TYPE_PHYSICAL, surveyForm.ID).Count(&physicalSurveyTotal)
+	// 	if physicalSurveyTotal > 0 {
+	// 		return &response.ErrCode{
+	// 			Code: response.ERROR,
+	// 			Msg:  "当前已设置有定制体检,请先修改原问卷的类型后再添加",
+	// 		}
+	// 	}
+	// }
+
+	err := model.DB.Model(model.Survey{}).Where("id = ?", surveyModel.ID).Select("name", "cover", "survey_code", "type").Updates(map[string]any{
+		"name":        surveyForm.Name,
+		"cover":       surveyForm.Cover,
+		"survey_code": surveyForm.SurveyCode,
+		"type":        surveyForm.Type,
+	}).Error
+	if err != nil {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "修改问卷失败",
+		}
+	}
+	return nil
+}
+
+// 获取问卷信息
+func Find(id int64) (*model.Survey, *response.ErrCode) {
+	var surveyModel model.Survey
+	err := model.DB.Where("id = ?", id).First(&surveyModel).Error
+	if err != nil {
+		return nil, &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "获取问卷信息失败",
+		}
+	}
+
+	return &surveyModel, nil
+}
+
+// 通过编号获取问卷信息
+func FindBySn(sn string) (*model.Survey, *response.ErrCode) {
+	var surveyModel model.Survey
+	err := model.DB.Where("sn = ?", sn).First(&surveyModel).Error
+	if err != nil {
+		return nil, &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "获取问卷信息失败",
+		}
+	}
+
+	return &surveyModel, nil
+}
+
+// 修改备注
+func UpdateRemark(sn string, remark string) *response.ErrCode {
+	// 检查是否存在
+	_, errCode := FindBySn(sn)
+	if errCode != nil {
+		return errCode
+	}
+
+	err := model.DB.Model(&model.Survey{}).Where("sn = ?", sn).Update("remark", remark).Error
+	if err != nil {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "修改备注失败",
+		}
+	}
+	return nil
+}
+
+// 修改状态
+func UpdateStatus(sn string, status int) *response.ErrCode {
+	if status != model.SURVEY_STATUS_ENABLE && status != model.SURVEY_STATUS_DISABLE {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "状态不在许可范围内",
+		}
+	}
+
+	// 检查是否存在
+	_, errCode := FindBySn(sn)
+	if errCode != nil {
+		return errCode
+	}
+
+	err := model.DB.Model(&model.Survey{}).Where("sn = ?", sn).Update("status", status).Error
+	if err != nil {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "修改状态失败",
+		}
+	}
+	return nil
+}
+
+// 获取列表
+func List(key string, surveyCode string, status int) []*model.Survey {
+	var surveyModels []*model.Survey
+
+	query := model.DB.Model(&model.Survey{})
+	if key != "" {
+		query = query.Where("name like @key or remark like @key", map[string]any{
+			"key": "%" + key + "%",
+		})
+	}
+	if surveyCode != "" {
+		query = query.Where("survey_code = ?", surveyCode)
+	}
+	if status != 0 {
+		query = query.Where("status = ?", status)
+	}
+
+	query.Order("id desc").Find(&surveyModels)
+
+	return surveyModels
+}
+
+// 分页获取列表
+func Paginate(page, pageSize int, key string, surveyCode string, status int) ([]*model.Survey, int64) {
+	var surveyModels []*model.Survey
+	var total int64
+
+	query := model.DB.Model(&model.Survey{})
+	if key != "" {
+		query = query.Where("name like @key or remark like @key", map[string]any{
+			"key": "%" + key + "%",
+		})
+	}
+	if surveyCode != "" {
+		query = query.Where("survey_code = ?", surveyCode)
+	}
+	if status != 0 {
+		query = query.Where("status = ?", status)
+	}
+
+	query.Count(&total)
+	query.Scopes(model.Paginate(page, pageSize)).Order("id desc").Find(&surveyModels)
+
+	return surveyModels, total
+}
+
+// 格式化
+func Format(surveyModel *model.Survey) *validators.Survey {
+	return &validators.Survey{
+		ID:         surveyModel.SN,
+		Name:       surveyModel.Name,
+		Cover:      surveyModel.Cover,
+		SurveyCode: surveyModel.SurveyCode,
+		Status:     surveyModel.Status,
+		Remark:     surveyModel.Remark,
+		Type:       surveyModel.Type,
+		CreatedAt:  carbon.Time2Carbon(surveyModel.CreatedAt).Format("Y/m/d H:i:s"),
+		UpdatedAt:  carbon.Time2Carbon(surveyModel.UpdatedAt).Format("Y/m/d H:i:s"),
+	}
+}

+ 38 - 0
service/survey/survey_test.go

@@ -0,0 +1,38 @@
+package survey_test
+
+import (
+	"fmt"
+	"surveyService/cache"
+	"surveyService/model"
+	"testing"
+
+	"github.com/joho/godotenv"
+	"github.com/samber/lo"
+)
+
+// 创建问卷
+func TestCreate(t *testing.T) {
+	// 加载dotEnv环境
+	loadEnvErr := godotenv.Load("/Users/huang/Desktop/hys/surveyService/.env")
+	if loadEnvErr != nil {
+		fmt.Println("ENV环境加载Error")
+		return
+	}
+	// 开始初始化数据库
+	model.Construct(false)
+
+	// 开始初始化缓存
+	cache.InitRedis()
+
+	var a = []string{"a", "b", "c"}
+	var b = []string{"a", "b"}
+	aa, ab := lo.Difference(a, b)
+
+	fmt.Println(aa, ab)
+
+	// survey.Create(&validators.Survey{
+	// 	Name:       "测试问卷",
+	// 	Cover:      "测试封面",
+	// 	SurveyCode: "测试问卷编码",
+	// })
+}

+ 242 - 0
service/survey_import/manage.go

@@ -0,0 +1,242 @@
+package survey_import
+
+import (
+	"context"
+	"fmt"
+	"surveyService/cache"
+	"surveyService/model"
+	"surveyService/response"
+	"surveyService/service/questionnaire_subject"
+	"surveyService/service/questionnaire_template"
+	"surveyService/util"
+	"surveyService/util/rabbitmq"
+	"surveyService/validators"
+
+	"github.com/guonaihong/gout"
+	jsoniter "github.com/json-iterator/go"
+	"gorm.io/gorm"
+)
+
+const (
+	ListQuestionnaireSubjectCacheKey      = "algor:questionnaireSubject:list"
+	HashListQuestionnaireSubjectCacheKey  = "algor:questionnaireSubject:hashList"
+	ListQuestionnaireTemplateCacheKey     = "algor:questionnaireTemplate:list"
+	HashListQuestionnaireTemplateCacheKey = "algor:questionnaireTemplate:hashList"
+)
+
+type SyncData struct {
+	Version       string `json:"version" form:"version" binding:"required"`
+	VersionNumber int64  `json:"versionNumber" form:"versionNumber" binding:"required"`
+	FileUrl       string `json:"fileUrl" form:"fileUrl" binding:"required"`
+	ID            int64  `json:"id" form:"id" binding:"required"`
+	CallbackUrl   string `json:"callbackUrl" form:"callbackUrl" binding:"required"`
+}
+
+type SyncSurveyResult struct {
+	Result *SyncSurveyData `json:"result"`
+}
+type SyncSurveyData struct {
+	Subjects  []*model.QuestionnaireSubject  `json:"subjects"`
+	Surveys   []*model.QuestionnaireSurvey   `json:"surveys"`
+	Templates []*model.QuestionnaireTemplate `json:"templates"`
+}
+
+var json = jsoniter.ConfigCompatibleWithStandardLibrary
+
+func StartSync(syncData SyncData) {
+	fmt.Println("收到的数据", syncData)
+	// 1. 调用同步接口
+	err := Sync(syncData)
+	if err != nil {
+		var webhookMessage rabbitmq.WebhookMessage
+		webhookMessage.Body = map[string]any{
+			"status": false,
+			"id":     syncData.ID,
+			"errMsg": err.Msg,
+		}
+		webhookMessage.Config.Url = syncData.CallbackUrl
+		fmt.Println("开始推送数据", util.JsonEncode(webhookMessage))
+		pushErr := rabbitmq.Webhook(webhookMessage)
+		if pushErr != nil {
+			// 更新失败
+			fmt.Println("数据推送失败", pushErr)
+			// Callback(dataSync.ID, false, "数据推送失败")
+			return
+		}
+		return
+	}
+	var webhookMessage rabbitmq.WebhookMessage
+	webhookMessage.Body = map[string]any{
+		"status": true,
+		"id":     syncData.ID,
+		"errMsg": "",
+	}
+	webhookMessage.Config.Url = syncData.CallbackUrl
+	fmt.Println("开始推送数据", util.JsonEncode(webhookMessage))
+	pushErr := rabbitmq.Webhook(webhookMessage)
+	if pushErr != nil {
+		// 更新失败
+		fmt.Println("数据推送失败", pushErr)
+		// Callback(dataSync.ID, false, "数据推送失败")
+		return
+	}
+}
+
+func Sync(syncData SyncData) *response.ErrCode {
+	// 下载文件
+	var body string
+	err := gout.GET(syncData.FileUrl).BindBody(&body).Do()
+	if err != nil {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "文件下载失败",
+		}
+	}
+
+	var result *SyncSurveyResult
+	json.UnmarshalFromString(body, &result)
+	if result == nil {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "文件解析失败",
+		}
+	}
+	var subjects []*model.QuestionnaireSubject = result.Result.Subjects
+	var templates []*model.QuestionnaireTemplate = result.Result.Templates
+	var templateSubjects []*model.QuestionnaireTemplateSubject = make([]*model.QuestionnaireTemplateSubject, 0)
+	for _, template := range templates {
+		for _, templateSubject := range template.SubjectRelations {
+			templateSubjects = append(templateSubjects, &model.QuestionnaireTemplateSubject{
+				TemplateID: templateSubject.TemplateID,
+				SubjectID:  templateSubject.SubjectID,
+				Sort:       templateSubject.Sort,
+			})
+		}
+		template.SubjectRelations = make([]*model.QuestionnaireTemplateSubject, 0)
+	}
+	var surveys []*model.QuestionnaireSurvey = result.Result.Surveys
+	var surveySubjects []*model.QuestionnaireSurveyQuestionnaireSubject = make([]*model.QuestionnaireSurveyQuestionnaireSubject, 0)
+	for _, survey := range surveys {
+		for _, surveySubject := range survey.Subjects {
+			surveySubjects = append(surveySubjects, &model.QuestionnaireSurveyQuestionnaireSubject{
+				SurveyID:   surveySubject.SurveyID,
+				SubjectID:  surveySubject.SubjectID,
+				Sort:       surveySubject.Sort,
+				IsRequired: surveySubject.IsRequired,
+			})
+		}
+		survey.Subjects = make([]*model.QuestionnaireSurveyQuestionnaireSubject, 0)
+	}
+
+	// 开启事务
+	updateErr := model.DB.Transaction(func(tx *gorm.DB) error {
+		// 删除subjects
+		model.DB.Model(&model.QuestionnaireSubject{}).Unscoped().Where("id > 0").Delete(&model.QuestionnaireSubject{})
+		// 删除templates
+		model.DB.Model(&model.QuestionnaireTemplate{}).Unscoped().Where("id > 0").Delete(&model.QuestionnaireTemplate{})
+		// 删除templateSubjects
+		model.DB.Model(&model.QuestionnaireTemplateSubject{}).Unscoped().Where("id > 0").Delete(&model.QuestionnaireTemplateSubject{})
+		// 删除surveys
+		model.DB.Model(&model.QuestionnaireSurvey{}).Unscoped().Where("id > 0").Delete(&model.QuestionnaireSurvey{})
+		// 删除surveySubjects
+		model.DB.Model(&model.QuestionnaireSurveyQuestionnaireSubject{}).Unscoped().Where("id > 0").Delete(&model.QuestionnaireSurveyQuestionnaireSubject{})
+		// 开始创建
+		if err := model.DB.Model(&model.QuestionnaireSubject{}).CreateInBatches(subjects, len(subjects)).Error; err != nil {
+			return fmt.Errorf("创建subjects失败: %w", err)
+		}
+		if err := model.DB.Model(&model.QuestionnaireTemplate{}).CreateInBatches(templates, len(templates)).Error; err != nil {
+			return fmt.Errorf("创建templates失败: %w", err)
+		}
+		if err := model.DB.Model(&model.QuestionnaireTemplateSubject{}).CreateInBatches(templateSubjects, len(templateSubjects)).Error; err != nil {
+			return fmt.Errorf("创建templateSubjects失败: %w", err)
+		}
+		if err := model.DB.Model(&model.QuestionnaireSurvey{}).CreateInBatches(surveys, len(surveys)).Error; err != nil {
+			return fmt.Errorf("创建surveys失败: %w", err)
+		}
+		if err := model.DB.Model(&model.QuestionnaireSurveyQuestionnaireSubject{}).CreateInBatches(surveySubjects, len(surveySubjects)).Error; err != nil {
+			return fmt.Errorf("创建surveySubjects失败: %w", err)
+		}
+		return nil
+	})
+	if updateErr != nil {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  updateErr.Error(),
+		}
+	}
+
+	go CacheList()
+
+	return nil
+}
+
+func CacheList() {
+	var subjectList = questionnaire_subject.List("")
+	var formatedSubjectList []*validators.QuestionnaireSubject
+	for _, subject := range subjectList {
+		formatedSubjectList = append(formatedSubjectList, questionnaire_subject.Format(subject))
+	}
+	CacheQuestionnaireSubject(formatedSubjectList)
+
+	var templateList = questionnaire_template.List("")
+	var formatedTemplateList []*validators.QuestionnaireTemplate
+	for _, template := range templateList {
+		formatedTemplateList = append(formatedTemplateList, questionnaire_template.Format(template))
+	}
+	CacheQuestionnaireTemplate(formatedTemplateList)
+}
+
+// 获取问题库列表
+func CacheQuestionnaireSubject(list []*validators.QuestionnaireSubject) *response.ErrCode {
+	// 删除键
+	cache.Instance().Delete(ListQuestionnaireSubjectCacheKey)
+	cache.Instance().Delete(HashListQuestionnaireSubjectCacheKey)
+
+	// 存缓存
+	listJson, marshalErr := json.Marshal(list)
+	if marshalErr != nil {
+		return response.Err
+	}
+	cache.Instance().PutStrForever(ListQuestionnaireSubjectCacheKey, string(listJson))
+
+	// 以ID为键,值为整体来存
+	var hashKeys = make([]any, 0)
+	for _, item := range list {
+		hashKeys = append(hashKeys, item.SN)
+		hashKeys = append(hashKeys, util.JsonEncode(item))
+	}
+	_, rdsErr := cache.GetClient().HMSet(context.Background(), cache.Instance().KeyWithPrefix(HashListQuestionnaireSubjectCacheKey), hashKeys...).Result()
+	if rdsErr != nil {
+		fmt.Println("同步失败")
+	} else {
+		fmt.Println("同步成功")
+	}
+	return nil
+}
+
+// 获取问题模板列表
+func CacheQuestionnaireTemplate(list []*validators.QuestionnaireTemplate) *response.ErrCode {
+	// 删除键
+	cache.Instance().Delete(ListQuestionnaireTemplateCacheKey)
+	cache.Instance().Delete(HashListQuestionnaireTemplateCacheKey)
+	// 存缓存
+	listJson, marshalErr := json.Marshal(list)
+	if marshalErr != nil {
+		return response.Err
+	}
+	cache.Instance().PutStrForever(ListQuestionnaireTemplateCacheKey, string(listJson))
+
+	// 以ID为键,值为整体来存
+	var hashKeys = make([]any, 0)
+	for _, item := range list {
+		hashKeys = append(hashKeys, item.SN)
+		hashKeys = append(hashKeys, util.JsonEncode(item))
+	}
+	_, rdsErr := cache.GetClient().HMSet(context.Background(), cache.Instance().KeyWithPrefix(HashListQuestionnaireTemplateCacheKey), hashKeys...).Result()
+	if rdsErr != nil {
+		fmt.Println("同步失败")
+	} else {
+		fmt.Println("同步成功")
+	}
+	return nil
+}

+ 21 - 0
service/survey_mechanism/external.go

@@ -0,0 +1,21 @@
+package survey_mechanism
+
+import (
+	"fmt"
+	"os"
+	"surveyService/response"
+	"surveyService/service/survey_token"
+)
+
+// 创建一个可以临时访问的链接
+func (m *Mechanism) GetSurveyVisitLink(surveyId string, extra string) (string, int64, *response.ErrCode) {
+	var timeout int64 = 7200
+	token := survey_token.InitMechanism(m.AuthMechanism).GenerateSurveyToken(surveyId, "", extra, timeout)
+	if token == "" {
+		return "", timeout, &response.ErrCode{
+			Msg:  "生成临时访问链接失败",
+			Code: response.ERROR,
+		}
+	}
+	return fmt.Sprintf("%s/#/%s/survey/decisionQuestion?id=%s&token=%s", os.Getenv("H5_DOMAIN"), m.Mechanism.ID, surveyId, token), timeout, nil
+}

+ 157 - 0
service/survey_mechanism/mechanism.go

@@ -0,0 +1,157 @@
+package survey_mechanism
+
+import (
+	"surveyService/model"
+	"surveyService/response"
+	"surveyService/service/questionnaire_survey"
+	"surveyService/validators"
+
+	"gogs.uu.mdfitnesscao.com/hys/sdk"
+)
+
+type Mechanism struct {
+	*sdk.AuthMechanism
+}
+
+func InitMechanism(authMechanism *sdk.AuthMechanism) *Mechanism {
+	return &Mechanism{authMechanism}
+}
+
+// 获取问卷详情
+func (m *Mechanism) Detail(sn string) (*model.SurveyMechanism, *response.ErrCode) {
+	var surveyMechanismModel model.SurveyMechanism
+	findErr := model.DB.Where("sn = ?", sn).Preload("Survey").Where("mechanism_id = ?", m.Mechanism.ID).First(&surveyMechanismModel).Error
+	if findErr != nil {
+		return nil, &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "没有找到有效的问卷信息",
+		}
+	}
+
+	// 检查当前问卷是否已被授权
+	if surveyMechanismModel.AuthorizeStatus != model.SURVEY_MECHANISM_AUTHORIZE_STATUS_ENABLE {
+		return nil, &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "当前问卷已被停止授权,无法操作该问卷",
+		}
+	}
+
+	return &surveyMechanismModel, nil
+}
+
+// 设置问卷信息
+func (m *Mechanism) Update(surveyMechanism *validators.SurveyMechanism) *response.ErrCode {
+	existsSurveyMechanism, findErr := m.Detail(surveyMechanism.ID)
+	if findErr != nil {
+		return findErr
+	}
+
+	// 开始修改
+	updateErr := model.DB.Where("id = ?", existsSurveyMechanism.ID).Select("name", "cover", "description").Updates(&model.SurveyMechanism{
+		Name:        surveyMechanism.Name,
+		Cover:       surveyMechanism.Cover,
+		Description: surveyMechanism.Description,
+	}).Error
+
+	if updateErr != nil {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "修改问卷信息失败",
+		}
+	}
+
+	return nil
+}
+
+// 修改问卷状态
+func (m *Mechanism) UpdateStatus(sn string, status int) *response.ErrCode {
+	if status != model.SURVEY_MECHANISM_STATUS_ENABLE && status != model.SURVEY_MECHANISM_STATUS_DISABLE {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "不支持的状态",
+		}
+	}
+	existsSurveyMechanism, findErr := m.Detail(sn)
+	if findErr != nil {
+		return findErr
+	}
+
+	// 修改状态
+	updateErr := model.DB.Model(model.SurveyMechanism{}).Where("id = ?", existsSurveyMechanism.ID).Update("status", status).Error
+	if updateErr != nil {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "修改状态失败",
+		}
+	}
+
+	return nil
+}
+
+// 获取问卷列表
+func (m *Mechanism) Paginate(page, pageSize int, status int, key string) ([]*model.SurveyMechanism, int64, *response.ErrCode) {
+	var surveyMechanismList []*model.SurveyMechanism
+	var count int64
+
+	// 开始查询
+	query := model.DB.Model(model.SurveyMechanism{}).Where("mechanism_id = ?", m.Mechanism.ID)
+	if status != 0 {
+		query = query.Where("status = ?", status)
+	}
+	if key != "" {
+		query = query.Where("name like @key or description like @key", map[string]any{
+			"key": "%" + key + "%",
+		})
+	}
+	query.Count(&count)
+	// 开始分页
+	query.Scopes(model.Paginate(page, pageSize)).Preload("Survey").Order("id desc").Find(&surveyMechanismList)
+
+	return surveyMechanismList, count, nil
+}
+
+// 获取问卷列表
+func (m *Mechanism) List(status int, key string) []*model.SurveyMechanism {
+	var surveyMechanismList []*model.SurveyMechanism
+	// 开始查询
+	query := model.DB.Model(model.SurveyMechanism{}).Where("mechanism_id = ?", m.Mechanism.ID)
+	if status != 0 {
+		query = query.Where("status = ?", status)
+	}
+	if key != "" {
+		query = query.Where("name like @key or description like @key", map[string]any{
+			"key": "%" + key + "%",
+		})
+	}
+	// 开始分页
+	query.Preload("Survey").Order("id desc").Find(&surveyMechanismList)
+
+	return surveyMechanismList
+}
+
+// 获取问卷详情
+func (m *Mechanism) DetailSurveyConfig(sn string) (*validators.QuestionnaireSurvey, *response.ErrCode) {
+	surveyMechanism, findErr := m.Detail(sn)
+	if findErr != nil {
+		return nil, findErr
+	}
+	// if surveyMechanism.Survey.SurveyCode == "" {
+	// 	return nil, &response.ErrCode{
+	// 		Code: response.ERROR,
+	// 		Msg:  "当前问卷未配置关联模型",
+	// 	}
+	// }
+	surveyCode := "DZTJV1"
+	if surveyMechanism.Survey.SurveyCode != "" {
+		surveyCode = surveyMechanism.Survey.SurveyCode
+	}
+	// 获取问卷配置信息
+	surveyConfig, detailErr := questionnaire_survey.FindBySN(surveyCode)
+	if detailErr != nil {
+		return nil, &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "获取问卷配置失败",
+		}
+	}
+	return questionnaire_survey.Format(surveyConfig), nil
+}

+ 133 - 0
service/survey_mechanism/member.go

@@ -0,0 +1,133 @@
+package survey_mechanism
+
+import (
+	"fmt"
+	"surveyService/model"
+	"surveyService/response"
+	"surveyService/service/questionnaire_survey"
+	"surveyService/service/survey"
+	"surveyService/validators"
+
+	"github.com/golang-module/carbon"
+	"gogs.uu.mdfitnesscao.com/hys/sdk"
+	"gorm.io/gorm"
+)
+
+type Member struct {
+	MechanismId string // 机构ID
+	Member      *sdk.AuthMember
+}
+
+func InitMember(member *sdk.AuthMember, mechanismId string) *Member {
+	return &Member{
+		MechanismId: mechanismId,
+		Member:      member,
+	}
+}
+
+// 获取问卷授权基本信息
+func (m *Member) Detail(sn string) (*model.SurveyMechanism, *response.ErrCode) {
+	var surveyMechanismModel *model.SurveyMechanism
+	findErr := model.DB.Where("sn = ?", sn).Preload("Survey").First(&surveyMechanismModel).Error
+	if findErr != nil {
+		return nil, &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "问卷不存在或已下架",
+		}
+	}
+	if surveyMechanismModel.Status != model.SURVEY_MECHANISM_STATUS_ENABLE {
+		return nil, &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "问卷未启用",
+		}
+	}
+	if surveyMechanismModel.AuthorizeStatus != model.SURVEY_MECHANISM_AUTHORIZE_STATUS_ENABLE {
+		return nil, &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "问卷授权被禁用",
+		}
+	}
+
+	return surveyMechanismModel, nil
+}
+
+// 获取授权的问卷列表
+func (m *Member) List(surveyType int64) []*model.SurveyMechanism {
+	surveyTableName := (&model.Survey{}).TableName()
+	surveyMechanismTableName := (&model.SurveyMechanism{}).TableName()
+	var surveyMechanismModels []*model.SurveyMechanism
+	var tx = model.DB.Preload("SurveyResult", func(db *gorm.DB) *gorm.DB {
+		return db.Where(&model.SurveyResult{
+			ArchivesId: m.Member.ID,
+		}).Order("end_time desc")
+	}).Preload("Survey").Where(&model.SurveyMechanism{
+		Status:          model.SURVEY_MECHANISM_STATUS_ENABLE,
+		AuthorizeStatus: model.SURVEY_MECHANISM_AUTHORIZE_STATUS_ENABLE,
+		MechanismId:     m.MechanismId,
+	}).Select(fmt.Sprintf("%s.*", surveyMechanismTableName)).Joins(fmt.Sprintf("right join %s on %s.id = %s.survey_id", surveyTableName, surveyTableName, surveyMechanismTableName))
+
+	if surveyType > 0 {
+		tx = tx.Where(fmt.Sprintf("%s.type = ?", surveyTableName), surveyType)
+	}
+	tx.Find(&surveyMechanismModels)
+
+	return surveyMechanismModels
+}
+
+// 获取问卷详情(转译系统)
+func (m *Member) DetailSurveyQuestions(sn string) (*validators.QuestionnaireSurvey, *response.ErrCode) {
+	_, findErr := m.Detail(sn)
+	if findErr != nil {
+		return nil, findErr
+	}
+	// if surveyMechanism.Survey.SurveyCode == "" {
+	// 	return nil, &response.ErrCode{
+	// 		Code: response.ERROR,
+	// 		Msg:  "该问卷未配置关联模型",
+	// 	}
+	// }
+
+	surveyCode := "DZTJV1"
+	// 获取问卷配置信息
+	surveyConfig, detailErr := questionnaire_survey.FindBySN(surveyCode)
+	if detailErr != nil {
+		return nil, &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "获取问卷配置失败",
+		}
+	}
+	return questionnaire_survey.Format(surveyConfig), nil
+}
+
+// 获取问卷详情(转译系统)
+func (m *Member) FormatSurvey(surveyMechanism *model.SurveyMechanism) *validators.SurveyMechanism {
+	if surveyMechanism == nil {
+		return nil
+	}
+	formatedSurvey := survey.Format(&surveyMechanism.Survey)
+	name := surveyMechanism.Name
+	cover := surveyMechanism.Cover
+	if name == "" {
+		name = formatedSurvey.Name
+	}
+	if cover == "" {
+		cover = formatedSurvey.Cover
+	}
+	lastVisitTime := ""
+	var lastSurveyResultId string = ""
+	if surveyMechanism.SurveyResult != nil {
+		lastVisitTime = carbon.Time2Carbon(surveyMechanism.SurveyResult.StartTime).Format("Y/m/d H:i:s")
+		lastSurveyResultId = surveyMechanism.SurveyResult.SN
+	}
+
+	return &validators.SurveyMechanism{
+		ID:                 surveyMechanism.SN,
+		MechanismId:        surveyMechanism.MechanismId,
+		Mechanism:          nil,
+		Name:               name,
+		Cover:              cover,
+		LastVisitTime:      lastVisitTime,
+		LastSurveyResultId: lastSurveyResultId,
+		Description:        surveyMechanism.Description,
+	}
+}

+ 214 - 0
service/survey_mechanism/survey_mechanism.go

@@ -0,0 +1,214 @@
+package survey_mechanism
+
+import (
+	"surveyService/model"
+	"surveyService/response"
+	"surveyService/service/survey"
+	"surveyService/validators"
+
+	"github.com/golang-module/carbon"
+	"github.com/samber/lo"
+	"gogs.uu.mdfitnesscao.com/hys/sdk"
+	"gogs.uu.mdfitnesscao.com/hys/sdk/mechanism"
+)
+
+// 获取某个问卷已被授权的机构列表
+func ListMechanismIdsForSurvey(surveyId string) ([]string, *response.ErrCode) {
+	var mechanismIds []string
+	// 检查问卷是否真实存在
+	surveyModel, findErr := survey.FindBySn(surveyId)
+	if findErr != nil {
+		return nil, findErr
+	}
+
+	model.DB.Where("survey_id = ?", surveyModel.ID).Pluck("mechanism_id", &mechanismIds)
+
+	return mechanismIds, nil
+}
+
+// 获取问卷授权信息
+func FindBySn(sn string) (*model.SurveyMechanism, *response.ErrCode) {
+	var surveyMechanismModel model.SurveyMechanism
+	findErr := model.DB.Where("sn = ?", sn).First(&surveyMechanismModel).Error
+	if findErr != nil {
+		return nil, &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "问卷授权不存在",
+		}
+	}
+
+	return &surveyMechanismModel, nil
+}
+
+// 开始创建问卷授权
+func Create(surveyId string, mechanismIds []string) *response.ErrCode {
+	// 检查问卷是否真实存在
+	surveyModel, findErr := survey.FindBySn(surveyId)
+	if findErr != nil {
+		return findErr
+	}
+
+	// 过滤掉所有跟问卷已经授权的机构
+	var existMechanismIds []string
+	model.DB.Model(&model.SurveyMechanism{}).Where("survey_id = ?", surveyModel.ID).Pluck("mechanism_id", &existMechanismIds)
+	mechanismIds, _ = lo.Difference(mechanismIds, existMechanismIds)
+
+	if len(mechanismIds) == 0 {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "没有需要授权的机构",
+		}
+	}
+
+	// 开始创建授权
+	var surveyMechanisms []*model.SurveyMechanism
+	for _, mechanismId := range mechanismIds {
+		surveyMechanisms = append(surveyMechanisms, &model.SurveyMechanism{
+			SurveyId:    surveyModel.ID,
+			MechanismId: mechanismId,
+		})
+	}
+	createErr := model.DB.CreateInBatches(surveyMechanisms, len(surveyMechanisms)).Error
+	if createErr != nil {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "创建问卷授权失败",
+		}
+	}
+
+	return nil
+}
+
+// 获取授权列表
+func Paginate(page, pageSize int, key string, mechanismId string, surveyId string) ([]*model.SurveyMechanism, int64, *response.ErrCode) {
+
+	var surveyMechanisms []*model.SurveyMechanism
+	var total int64
+
+	// 开始查询
+	query := model.DB.Model(&model.SurveyMechanism{})
+
+	// 模糊搜索
+	if key != "" {
+		query = query.Where("name LIKE @key or description like @key", map[string]any{
+			"key": "%" + key + "%",
+		})
+	}
+
+	// 机构ID
+	if mechanismId != "" {
+		query = query.Where("mechanism_id = ?", mechanismId)
+	}
+
+	// 问卷ID
+	if surveyId != "" {
+		// 检查问卷是否真实存在
+		surveyModel, findErr := survey.FindBySn(surveyId)
+		if findErr != nil {
+			return surveyMechanisms, total, findErr
+		}
+		query = query.Where("survey_id = ?", surveyModel.ID)
+	}
+
+	// 开始分页
+	paginateErr := query.Count(&total).Error
+	if paginateErr != nil {
+		return nil, 0, &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "获取授权列表失败",
+		}
+	}
+
+	query.Scopes(model.Paginate(page, pageSize)).Order("id desc").Preload("Survey").Find(&surveyMechanisms)
+	return surveyMechanisms, total, nil
+}
+
+// 修改授权状态
+func UpdateAuthorizeStatus(sn string, status int) *response.ErrCode {
+	if status != model.SURVEY_MECHANISM_AUTHORIZE_STATUS_ENABLE && status != model.SURVEY_MECHANISM_AUTHORIZE_STATUS_DISABLE {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "不支持的状态",
+		}
+	}
+	existsSurveyMechanism, findErr := FindBySn(sn)
+	if findErr != nil {
+		return findErr
+	}
+
+	// 修改状态
+	updateErr := model.DB.Model(model.SurveyMechanism{}).Where("id = ?", existsSurveyMechanism.ID).Update("authorize_status", status).Error
+	if updateErr != nil {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "修改授权状态失败",
+		}
+	}
+
+	return nil
+}
+
+// 批量格式化
+func ListFormat(surveyMechanisms []*model.SurveyMechanism) []*validators.SurveyMechanism {
+	formatedSurveyMechanisms := make([]*validators.SurveyMechanism, 0)
+	var mechanismIds []string
+	for _, surveyMechanism := range surveyMechanisms {
+		if surveyMechanism != nil {
+			mechanismIds = append(mechanismIds, surveyMechanism.MechanismId)
+		}
+	}
+	mechanismIds = lo.Uniq(mechanismIds)
+
+	// 获取机构列表
+	mechanisms, _ := mechanism.ListMechanismByIds(mechanismIds)
+
+	for _, surveyMechanism := range surveyMechanisms {
+		var mechanism *sdk.Mechanism
+		// 找到机构
+		_, index, _ := lo.FindIndexOf(mechanisms, func(mechanismItem *sdk.Mechanism) bool {
+			return mechanismItem.ID == surveyMechanism.MechanismId
+		})
+		if index >= 0 {
+			mechanism = mechanisms[index]
+			formatedSurveyMechanisms = append(formatedSurveyMechanisms, Format(surveyMechanism, mechanism, true))
+		}
+	}
+
+	return formatedSurveyMechanisms
+}
+
+// 格式化
+func Format(surveyMechanism *model.SurveyMechanism, mechanism *sdk.Mechanism, visibleSurvey bool) *validators.SurveyMechanism {
+	if surveyMechanism == nil {
+		return nil
+	}
+	formatedSurvey := survey.Format(&surveyMechanism.Survey)
+	surveyId := formatedSurvey.ID
+	name := surveyMechanism.Name
+	cover := surveyMechanism.Cover
+	if name == "" {
+		name = formatedSurvey.Name
+	}
+	if cover == "" {
+		cover = formatedSurvey.Cover
+	}
+	if !visibleSurvey {
+		formatedSurvey = nil
+		surveyId = ""
+	}
+	return &validators.SurveyMechanism{
+		ID:              surveyMechanism.SN,
+		AuthorizeStatus: surveyMechanism.AuthorizeStatus,
+		Status:          surveyMechanism.Status,
+		MechanismId:     surveyMechanism.MechanismId,
+		Mechanism:       mechanism,
+		SurveyId:        surveyId,
+		Survey:          formatedSurvey,
+		CreatedAt:       carbon.Time2Carbon(surveyMechanism.CreatedAt).Format("Y/m/d H:i:s"),
+		UpdatedAt:       carbon.Time2Carbon(surveyMechanism.UpdatedAt).Format("Y/m/d H:i:s"),
+		Permissions:     surveyMechanism.Permissions,
+		Name:            name,
+		Cover:           cover,
+		Description:     surveyMechanism.Description,
+	}
+}

+ 12 - 0
service/survey_mechanism/survey_mechanism_test.go

@@ -0,0 +1,12 @@
+package survey_mechanism_test
+
+import (
+	"surveyService/service/survey_mechanism"
+	"surveyService/tests"
+	"testing"
+)
+
+func TestCreate(t *testing.T) {
+	tests.Init()
+	survey_mechanism.Create("K2DVZDQA", []string{"Xwlg97rP"})
+}

+ 325 - 0
service/survey_result/member.go

@@ -0,0 +1,325 @@
+package survey_result
+
+import (
+	"fmt"
+	"surveyService/model"
+	"surveyService/response"
+	"surveyService/sdk/survey_disease"
+	"surveyService/service/survey"
+	"surveyService/service/survey_mechanism"
+	"surveyService/util"
+	"surveyService/validators"
+
+	"github.com/golang-module/carbon"
+	jsoniter "github.com/json-iterator/go"
+	"gogs.uu.mdfitnesscao.com/hys/sdk"
+)
+
+type Member struct {
+	MechanismId string // 机构ID
+	Member      *sdk.AuthMember
+}
+
+func InitMember(member *sdk.AuthMember, mechanismId string) *Member {
+	return &Member{
+		MechanismId: mechanismId,
+		Member:      member,
+	}
+}
+
+// 获取问卷结果
+func (m *Member) Detail(sn string) (*model.SurveyResult, *response.ErrCode) {
+	var surveyResult *model.SurveyResult
+	tx := model.DB.Where("sn = ?", sn)
+	if m.Member != nil {
+		tx = tx.Where("archives_id = ?", m.Member.ID)
+	}
+
+	err := tx.Preload("SurveyMechanism").First(&surveyResult).Error
+	if err != nil {
+		return nil, &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "获取问卷结果失败",
+		}
+	}
+
+	if m.MechanismId != surveyResult.SurveyMechanism.MechanismId {
+		return nil, &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "没有找到有效的问卷结果数据",
+		}
+	}
+
+	return surveyResult, nil
+}
+
+// 获取最后一份问卷结果
+func (m *Member) DetailLastSurvey(surveyMechanismId int64, isPhysicalSurvey bool) (*model.SurveyResult, *response.ErrCode) {
+	var surveyResult *model.SurveyResult
+
+	// 如果是定制体检
+	if isPhysicalSurvey {
+		var physicalSurver *model.Survey
+		// 查出哪份问卷是定制体检
+		err := model.DB.Model(&model.Survey{}).Where(&model.Survey{
+			Type: model.SURVEY_TYPE_PHYSICAL,
+		}).First(&physicalSurver).Error
+		if err != nil {
+			return nil, &response.ErrCode{
+				Code: response.ERROR,
+				Msg:  "定制体检问卷未设置",
+			}
+		}
+		var surveyMechanism *model.SurveyMechanism
+		// 开始查找机构授权的问卷
+		err = model.DB.Model(&model.SurveyMechanism{}).Where(&model.SurveyMechanism{
+			SurveyId:    physicalSurver.ID,
+			MechanismId: m.MechanismId,
+		}).First(&surveyMechanism).Error
+		if err != nil {
+			return nil, &response.ErrCode{
+				Code: response.ERROR,
+				Msg:  "机构未授权定制体检问卷",
+			}
+		}
+		surveyMechanismId = surveyMechanism.ID
+	}
+
+	tx := model.DB.Where(&model.SurveyResult{
+		SurveyMechanismId: surveyMechanismId,
+	})
+	if m.Member != nil {
+		tx = tx.Where("archives_id = ?", m.Member.ID)
+	}
+
+	err := tx.Preload("SurveyMechanism").First(&surveyResult).Error
+	if err != nil {
+		return nil, &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "获取问卷结果失败",
+		}
+	}
+
+	if m.MechanismId != surveyResult.SurveyMechanism.MechanismId {
+		return nil, &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "没有找到有效的问卷结果数据",
+		}
+	}
+
+	return surveyResult, nil
+}
+
+// 获取问卷结果
+func (m *Member) Paginate(page, pageSize int) ([]*model.SurveyResult, int64) {
+	var surveyResults []*model.SurveyResult
+	var total int64
+	tx := model.DB.Model(&model.SurveyResult{})
+	if m.Member != nil {
+		tx = tx.Where("archives_id = ?", m.Member.ID)
+	}
+	var surveyMechanismTableName = (&model.SurveyMechanism{}).TableName()
+	var surveyResultTableName = (&model.SurveyResult{}).TableName()
+	tx = tx.Joins(fmt.Sprintf("left join %s on %s.id = %s.survey_mechanism_id", surveyMechanismTableName, surveyMechanismTableName, surveyResultTableName)).Where(fmt.Sprintf("%s.mechanism_id = ?", surveyMechanismTableName), m.MechanismId)
+	tx.Count(&total)
+	tx.Select(fmt.Sprintf("%s.*", surveyResultTableName)).Scopes(model.Paginate(page, pageSize)).Order(fmt.Sprintf("%s.id desc", surveyResultTableName)).Preload("SurveyMechanism").Find(&surveyResults)
+
+	return surveyResults, total
+}
+
+// 创建问卷结果
+// surveyMechanismId 机构问卷编号
+func (m *Member) Create(surveyMechanismId string, extra string) (*model.SurveyResult, *response.ErrCode) {
+	// 检查问卷是否在
+	surveyMechanism, findErr := survey_mechanism.InitMember(m.Member, m.MechanismId).Detail(surveyMechanismId)
+	if findErr != nil {
+		return nil, findErr
+	}
+
+	// 检查是否有未完成的问卷
+	if extra != "" {
+		var existsSurveyResult *model.SurveyResult
+		model.DB.Where("extra = ?", extra).First(&existsSurveyResult)
+		if existsSurveyResult != nil {
+			// 检查是否可以继续作答
+			if existsSurveyResult.Status == model.SurveyResultStatusWait {
+				return existsSurveyResult, nil
+			}
+		}
+	}
+
+	archivesId := ""
+	if m.Member != nil {
+		archivesId = m.Member.ID
+	}
+
+	surveyResultModel := model.SurveyResult{
+		StartTime:         carbon.Now().Carbon2Time(),
+		SurveyMechanismId: surveyMechanism.ID,
+		Status:            model.SurveyResultStatusWait,
+		Method:            model.SurveyResultMethodArchives,
+		MechanismId:       m.MechanismId,
+		ArchivesId:        archivesId,
+		Extra:             extra,
+	}
+
+	err := model.DB.Create(&surveyResultModel).Error
+	if err != nil {
+		return nil, &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "创建问卷结果失败",
+		}
+	}
+
+	return &surveyResultModel, nil
+}
+
+// 提交答题结果
+func (m *Member) Submit(sn string, answers map[string]any) *response.ErrCode {
+	// 获取问卷结果
+	surveyResult, findErr := m.Detail(sn)
+	if findErr != nil {
+		return findErr
+	}
+
+	// 只有在填写中的问卷才能保存
+	if surveyResult.Status != model.SurveyResultStatusWait {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "问卷结果已经提交,不能再保存",
+		}
+	}
+
+	if m.Member != nil {
+		if surveyResult.ArchivesId != m.Member.ID {
+			return &response.ErrCode{
+				Code: response.ERROR,
+				Msg:  "答题问卷不存在",
+			}
+		}
+	}
+
+	// 只有后台新增的才可以修改
+	// if surveyResult.Method == model.SurveyResultMethodArchives {
+	// 	return &response.ErrCode{
+	// 		Code: response.ERROR,
+	// 		Msg:  "当前问卷不支持修改答题结果",
+	// 	}
+	// }
+
+	// 保存问卷结果
+	updateErr := model.DB.Model(model.SurveyResult{}).Where("id = ?", surveyResult.ID).Select([]string{"AnswerRaw", "Status"}).Updates(&model.SurveyResult{
+		AnswerRaw: util.JsonEncode(answers),
+		Status:    model.SurveyResultStatusFulled,
+		EndTime:   carbon.Now().Carbon2Time(),
+	}).Error
+	if updateErr != nil {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "提交问卷失败",
+		}
+	}
+
+	_, findErr = survey.Find(surveyResult.SurveyMechanism.SurveyId)
+	if findErr != nil {
+		return findErr
+	}
+
+	// 提交问卷结果数据
+	go func() {
+
+		// 将answer转成指定格式
+		var formatedAnswers map[string]*sdk.SurveyAnswer
+		jsoniter.UnmarshalFromString(util.JsonEncode(answers), &formatedAnswers)
+		surveyDiseaseCalcResult := survey_disease.DiseaseScreeningCal(
+			formatedAnswers,
+		)
+		ProcessResult(sn, surveyDiseaseCalcResult)
+	}()
+
+	return nil
+}
+
+// 创建并提交问卷结果
+func (m *Member) CreateAndSubmit(surveyMechanismId string, answers map[string]any, startTime string, extra string) (*model.SurveyResult, *response.ErrCode) {
+	// 检查问卷是否在
+	surveyMechanism, findErr := survey_mechanism.InitMember(m.Member, m.MechanismId).Detail(surveyMechanismId)
+	if findErr != nil {
+		return nil, findErr
+	}
+
+	archivesId := ""
+	if m.Member != nil {
+		archivesId = m.Member.ID
+	}
+
+	// 如果开始时间大于当前时间,报错
+	startTimeCarbon := carbon.Parse(startTime)
+	if startTimeCarbon.Gte(carbon.Now()) {
+		return nil, &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "开始时间不能大于当前时间",
+		}
+	}
+
+	surveyResultModel := &model.SurveyResult{
+		StartTime:         startTimeCarbon.Carbon2Time(),
+		SurveyMechanismId: surveyMechanism.ID,
+		Method:            model.SurveyResultMethodArchives,
+		MechanismId:       m.MechanismId,
+		ArchivesId:        archivesId,
+		Extra:             extra,
+		AnswerRaw:         util.JsonEncode(answers),
+		Status:            model.SurveyResultStatusFulled,
+		EndTime:           carbon.Now().Carbon2Time(),
+	}
+
+	err := model.DB.Save(&surveyResultModel).Error
+	if err != nil {
+		return nil, &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "问卷提交失败",
+		}
+	}
+
+	_, findErr = survey.Find(surveyMechanism.SurveyId)
+	if findErr != nil {
+		return nil, findErr
+	}
+
+	go func() {
+		var formatedAnswers map[string]*sdk.SurveyAnswer
+		jsoniter.UnmarshalFromString(util.JsonEncode(answers), &formatedAnswers)
+		surveyDiseaseCalcResult := survey_disease.DiseaseScreeningCal(
+			formatedAnswers,
+		)
+		ProcessResult(surveyResultModel.SN, surveyDiseaseCalcResult)
+	}()
+	return surveyResultModel, nil
+}
+
+// 格式化答题结果
+func (m *Member) Format(rawSurveyResult *model.SurveyResult, needResult bool) *validators.SurveyResult {
+	var answerResult map[string]any
+	var resultRaw string
+	if needResult {
+		if rawSurveyResult.AnswerRaw != "" {
+			json.UnmarshalFromString(rawSurveyResult.AnswerRaw, &answerResult)
+		}
+		resultRaw = rawSurveyResult.ResultRaw
+	}
+	return &validators.SurveyResult{
+		ID:                rawSurveyResult.SN,
+		Method:            rawSurveyResult.Method,
+		StartTime:         carbon.Time2Carbon(rawSurveyResult.StartTime).Format("Y/m/d H:i:s"),
+		EndTime:           carbon.Time2Carbon(rawSurveyResult.EndTime).Format("Y/m/d H:i:s"),
+		Status:            rawSurveyResult.Status,
+		ArchivesId:        rawSurveyResult.ArchivesId,
+		AnswerResult:      answerResult,
+		ResultRaw:         resultRaw,
+		SurveyMechanismId: (&model.SurveyMechanism{}).GetHashId(rawSurveyResult.SurveyMechanismId),
+		SurveyMechanism:   survey_mechanism.ListFormat([]*model.SurveyMechanism{rawSurveyResult.SurveyMechanism})[0],
+		CreatedAt:         carbon.Time2Carbon(rawSurveyResult.CreatedAt).Format("Y/m/d H:i:s"),
+		UpdatedAt:         carbon.Time2Carbon(rawSurveyResult.UpdatedAt).Format("Y/m/d H:i:s"),
+	}
+}

+ 372 - 0
service/survey_result/survey_result.go

@@ -0,0 +1,372 @@
+package survey_result
+
+import (
+	"fmt"
+	"os"
+	"strings"
+	"surveyService/model"
+	"surveyService/response"
+	"surveyService/sdk/survey_disease"
+	"surveyService/service/survey"
+	"surveyService/service/survey_mechanism"
+	"surveyService/service/survey_token"
+	"surveyService/util"
+	"surveyService/util/rabbitmq"
+	"surveyService/validators"
+
+	"github.com/golang-module/carbon"
+	jsoniter "github.com/json-iterator/go"
+	"gogs.uu.mdfitnesscao.com/hys/sdk"
+	"gogs.uu.mdfitnesscao.com/hys/sdk/mechanism"
+)
+
+var json = jsoniter.ConfigCompatibleWithStandardLibrary
+
+type Mechanism struct {
+	*sdk.AuthMechanism
+}
+
+func InitMechanism(authMechanism *sdk.AuthMechanism) *Mechanism {
+	return &Mechanism{authMechanism}
+}
+
+// 获取问卷结果
+func (m *Mechanism) Detail(sn string) (*model.SurveyResult, *response.ErrCode) {
+	var surveyResult *model.SurveyResult
+	err := model.DB.Where("sn = ?", sn).Preload("SurveyMechanism").First(&surveyResult).Error
+	if err != nil {
+		return nil, &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "获取问卷结果失败",
+		}
+	}
+
+	if m.AuthMechanism != nil && m.Mechanism.ID != surveyResult.SurveyMechanism.MechanismId {
+		return nil, &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "没有找到有效的问卷结果数据",
+		}
+	}
+
+	return surveyResult, nil
+}
+
+// // 获取某个问卷最后一份的结果
+// func (m *Mechanism) LastResult(surveyMechanismId string) (*model.SurveyResult, *response.ErrCode) {
+
+// }
+
+// 获取问卷结果
+func (m *Mechanism) DetailByExtra(extra string) (*model.SurveyResult, *response.ErrCode) {
+	tx := model.DB.Model(&model.SurveyResult{})
+	var surveyResult *model.SurveyResult
+	err := tx.Where("extra = ?", extra).Preload("SurveyMechanism").First(&surveyResult).Error
+	if err != nil {
+		return nil, &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "获取问卷结果失败",
+		}
+	}
+
+	if m.AuthMechanism != nil && m.Mechanism.ID != surveyResult.SurveyMechanism.MechanismId {
+		return nil, &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "没有找到有效的问卷结果数据",
+		}
+	}
+
+	return surveyResult, nil
+}
+
+// 获取问卷结果列表
+func (m *Mechanism) Paginate(queryData validators.SurveyResultPaginate) ([]*model.SurveyResult, int64, *response.ErrCode) {
+	var surveyResults []*model.SurveyResult = make([]*model.SurveyResult, 0)
+	var total int64
+
+	query := model.DB.Model(model.SurveyResult{})
+	if queryData.Method != 0 {
+		query = query.Where("method = ?", queryData.Method)
+	}
+	if queryData.SurveyMechanismId != "" {
+		surveyMechanismRawId := (&model.SurveyMechanism{}).GetRawId(queryData.SurveyMechanismId)
+		if surveyMechanismRawId == 0 {
+			return surveyResults, total, &response.ErrCode{
+				Code: response.ERROR,
+				Msg:  "无效的问卷编号",
+			}
+		}
+		query = query.Where("survey_mechanism_id = ?", surveyMechanismRawId)
+	}
+	if queryData.ArchivesId != "" {
+		query = query.Where("archives_id = ?", queryData.ArchivesId)
+	}
+	if m.AuthMechanism == nil {
+		if queryData.SurveyId != "" {
+			surveyRawId := (&model.Survey{}).GetRawId(queryData.SurveyId)
+			if surveyRawId == 0 {
+				return surveyResults, total, &response.ErrCode{
+					Code: response.ERROR,
+					Msg:  "无效的问卷编号",
+				}
+			}
+			query = query.Where("survey_mechanism_id in ?", model.DB.Where("survey_id = ?", surveyRawId).Select("id").Model(&model.SurveyMechanism{}))
+		}
+	}
+	mechanismId := ""
+	if m.AuthMechanism != nil {
+		mechanismId = m.Mechanism.ID
+	}
+	query.Scopes(model.MechanismQuery(mechanismId)).Count(&total)
+	if total == 0 {
+		return surveyResults, total, nil
+	}
+	query.Scopes(model.Paginate(queryData.Page, queryData.PageSize)).Scopes(model.MechanismQuery(mechanismId)).Preload("SurveyMechanism").Order("start_time desc").Find(&surveyResults)
+
+	return surveyResults, total, nil
+}
+
+// 修改答题人
+func (m *Mechanism) UpdateArchivesId(sn string, archivesId string) *response.ErrCode {
+	// 获取问卷结果数据
+	surveyResult, findErr := m.Detail(sn)
+	if findErr != nil {
+		return findErr
+	}
+
+	// Todo 检查档案是否有效
+
+	// 只有后台新增的才可以修改
+	if surveyResult.Method == model.SurveyResultMethodArchives {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "当前问卷不允许修改答题人",
+		}
+	}
+
+	// 保存问卷结果
+	updateErr := model.DB.Model(model.SurveyResult{}).Where("id = ?", surveyResult.ID).Update("archives_id", archivesId).Error
+	if updateErr != nil {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "保存问卷结果失败",
+		}
+	}
+
+	return nil
+}
+
+// 重新提交到转译系统
+func (m *Mechanism) Run(sn string) *response.ErrCode {
+	// 获取问卷结果
+	surveyResult, findErr := m.Detail(sn)
+	if findErr != nil {
+		return findErr
+	}
+
+	var answers map[string]any
+	fmt.Println(surveyResult.AnswerRaw)
+	unmarshalErr := json.UnmarshalFromString(surveyResult.AnswerRaw, &answers)
+	if unmarshalErr != nil {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "无效的答案",
+		}
+	}
+
+	_, findErr = survey.Find(surveyResult.SurveyMechanism.SurveyId)
+	if findErr != nil {
+		return findErr
+	}
+
+	var formatedAnswers map[string]*sdk.SurveyAnswer
+	jsoniter.UnmarshalFromString(util.JsonEncode(answers), &formatedAnswers)
+	surveyDiseaseCalcResult := survey_disease.DiseaseScreeningCal(
+		formatedAnswers,
+	)
+	ProcessResult(sn, surveyDiseaseCalcResult)
+	return nil
+}
+
+// 格式化答题结果
+func Format(rawSurveyResult *model.SurveyResult, needResult bool) *validators.SurveyResult {
+	var answerResult map[string]any
+	var resultRaw string
+	if needResult {
+		if rawSurveyResult.AnswerRaw != "" {
+			json.UnmarshalFromString(rawSurveyResult.AnswerRaw, &answerResult)
+		}
+		resultRaw = rawSurveyResult.ResultRaw
+	}
+	return &validators.SurveyResult{
+		ID:                rawSurveyResult.SN,
+		Method:            rawSurveyResult.Method,
+		StartTime:         carbon.Time2Carbon(rawSurveyResult.StartTime).Format("Y/m/d H:i:s"),
+		EndTime:           carbon.Time2Carbon(rawSurveyResult.EndTime).Format("Y/m/d H:i:s"),
+		Status:            rawSurveyResult.Status,
+		ArchivesId:        rawSurveyResult.ArchivesId,
+		AnswerResult:      answerResult,
+		ResultRaw:         resultRaw,
+		CanAnalyze:        rawSurveyResult.AnswerRaw != "",
+		SurveyMechanismId: (&model.SurveyMechanism{}).GetHashId(rawSurveyResult.SurveyMechanismId),
+		SurveyMechanism:   survey_mechanism.ListFormat([]*model.SurveyMechanism{rawSurveyResult.SurveyMechanism})[0],
+		CreatedAt:         carbon.Time2Carbon(rawSurveyResult.CreatedAt).Format("Y/m/d H:i:s"),
+		UpdatedAt:         carbon.Time2Carbon(rawSurveyResult.UpdatedAt).Format("Y/m/d H:i:s"),
+	}
+}
+
+// 接收问卷结果回调
+func ReceiveResult(modelSn, extra string, modelType int, data map[string]any) *response.ErrCode {
+	if modelType != 1 {
+		return nil
+	}
+	// 获取问卷结果数据
+	surveyResult, findErr := InitMechanism(nil).Detail(extra)
+	if findErr != nil {
+		return findErr
+	}
+
+	// 只有在填写中的问卷才能保存
+	if surveyResult.Status != model.SurveyResultStatusFulled {
+		fmt.Println("当前问卷状态不支持保存结果")
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "当前问卷状态不支持保存结果",
+		}
+	}
+
+	// 保存问卷结果
+	updateErr := model.DB.Model(model.SurveyResult{}).Where("id = ?", surveyResult.ID).Select([]string{"ResultRaw", "Status"}).Updates(&model.SurveyResult{
+		ResultRaw: util.JsonEncode(data),
+		Status:    model.SurveyResultStatusDone,
+	}).Error
+	if updateErr != nil {
+		fmt.Println("保存问卷结果失败")
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "保存问卷结果失败",
+		}
+	}
+
+	// 开始回调机构配置的回调地址
+	go CallbackSurveyResultToMechanism(surveyResult.SN)
+
+	return nil
+}
+
+// 接收问卷结果回调
+func ProcessResult(sn string, data *survey_disease.SurveyResult) *response.ErrCode {
+	// 获取问卷结果数据
+	surveyResult, findErr := InitMechanism(nil).Detail(sn)
+	if findErr != nil {
+		return findErr
+	}
+
+	// 只有在填写中的问卷才能保存
+	if surveyResult.Status != model.SurveyResultStatusFulled {
+		fmt.Println("当前问卷状态不支持保存结果")
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "当前问卷状态不支持保存结果",
+		}
+	}
+
+	// 保存问卷结果
+	updateErr := model.DB.Model(model.SurveyResult{}).Where("id = ?", surveyResult.ID).Select([]string{"ResultRaw", "Status"}).Updates(&model.SurveyResult{
+		ResultRaw: util.JsonEncode(map[string]any{
+			"result": data,
+		}),
+		Status: model.SurveyResultStatusDone,
+	}).Error
+	if updateErr != nil {
+		fmt.Println("保存问卷结果失败")
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "保存问卷结果失败",
+		}
+	}
+
+	// 开始回调机构配置的回调地址
+	go CallbackSurveyResultToMechanism(surveyResult.SN)
+
+	return nil
+}
+
+// 将PDF报告推送至机构端
+func CallbackSurveyResultToMechanism(surveyResultId string) *response.ErrCode {
+	surveyResult, findSurveyResultErr := InitMechanism(nil).Detail(surveyResultId)
+	if findSurveyResultErr != nil {
+		return findSurveyResultErr
+	}
+	if surveyResult.Extra == "" {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "无自定义参数,不回调",
+		}
+	}
+
+	var callbackData map[string]any = map[string]any{
+		"dataType": "Survey",
+		"errMsg":   "",
+		"extra":    surveyResult.Extra,
+		"surveyId": surveyResult.SurveyMechanism.SN,
+		"status":   surveyResult.Status,
+	}
+
+	// 检查机构是否配置了回调地址
+	mechanismListResponse, findErr := mechanism.ListMechanism([]string{surveyResult.MechanismId})
+	if findErr != nil {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "获取机构信息失败: " + findErr.Msg,
+		}
+	}
+	if len(mechanismListResponse.Data.List) == 0 {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "没有找到机构信息",
+		}
+	}
+
+	// 开始推送
+	var mechanismInfo = mechanismListResponse.Data.List[0]
+	if mechanismInfo.CallbackUrlList == "" {
+		return &response.ErrCode{
+			Code: response.ERROR,
+			Msg:  "机构没有配置回调地址",
+		}
+	}
+
+	// 开始推送
+	var callbackUrlList []string = strings.Split(mechanismInfo.CallbackUrlList, "\n")
+	for _, callbackUrl := range callbackUrlList {
+		if callbackUrl == "" {
+			continue
+		}
+		var webhookMessage rabbitmq.WebhookMessage
+		webhookMessage.Body = callbackData
+		webhookMessage.Config.Url = callbackUrl
+		rabbitmq.Webhook(webhookMessage)
+	}
+
+	return nil
+}
+
+// 创建一个可以临时访问结果的链接
+func (m *Mechanism) GetSurveyResultVisitLink(extra string) (string, int64, *model.SurveyResult, *response.ErrCode) {
+	var timeout int64 = 7200
+	// 检查问卷是否已被完成
+	surveyResult, findErr := m.DetailByExtra(extra)
+	if findErr != nil {
+		return "", timeout, nil, findErr
+	}
+
+	// 检查问卷结果是否存在
+	token := survey_token.InitMechanism(m.AuthMechanism).GenerateSurveyToken(surveyResult.SurveyMechanism.SN, surveyResult.SN, extra, timeout)
+	if token == "" {
+		return "", timeout, surveyResult, &response.ErrCode{
+			Msg:  "生成临时访问链接失败",
+			Code: response.ERROR,
+		}
+	}
+	return fmt.Sprintf("%s/#/%s/survey/result?id=%s&surveyMechanismId=%s&token=%s", os.Getenv("H5_DOMAIN"), m.Mechanism.ID, surveyResult.SN, surveyResult.SurveyMechanism.SN, token), timeout, surveyResult, nil
+}

+ 12 - 0
service/survey_result/survey_result_test.go

@@ -0,0 +1,12 @@
+package survey_result_test
+
+import (
+	"surveyService/service/survey_result"
+	"surveyService/tests"
+	"testing"
+)
+
+func TestXxx(t *testing.T) {
+	tests.Init()
+	survey_result.InitMechanism(nil).Run("AVWbXM1L")
+}

+ 89 - 0
service/survey_token/survey_token.go

@@ -0,0 +1,89 @@
+package survey_token
+
+import (
+	"fmt"
+	"surveyService/cache"
+	"surveyService/response"
+	"surveyService/util"
+	"surveyService/validators"
+	"time"
+
+	jsoniter "github.com/json-iterator/go"
+	"gogs.uu.mdfitnesscao.com/hys/sdk"
+)
+
+var json = jsoniter.ConfigCompatibleWithStandardLibrary
+
+type Mechanism struct {
+	*sdk.AuthMechanism
+}
+
+func InitMechanism(authMechanism *sdk.AuthMechanism) *Mechanism {
+	return &Mechanism{authMechanism}
+}
+
+// 生成一个临时凭证
+func (m *Mechanism) GenerateSurveyToken(surveyId, surveyResultId, extra string, expired int64) string {
+	var tokenData = &validators.SurveyToken{
+		Extra:          extra,
+		SurveyId:       surveyId,
+		MechanismId:    m.Mechanism.ID,
+		SurveyResultId: surveyResultId,
+	}
+	token := util.RandString(12)
+	cacheKey := fmt.Sprintf("survey-result:token:%s", token)
+	expiredAt := time.Duration(expired) * time.Second
+	// 保存到redis
+	cacheErr := cache.Instance().Put(cacheKey, tokenData, expiredAt)
+	if cacheErr != nil {
+		return ""
+	}
+	return token
+}
+
+// 重新修改凭证里存储的数据
+func RenewSurveyToken(token string, renewSurveyToken *validators.SurveyToken, expired int64) *response.ErrCode {
+	tokenData, getErr := GetSurveyTokenData(token)
+	if getErr != nil {
+		return getErr
+	}
+
+	var renewTokenData = &validators.SurveyToken{
+		Extra:          tokenData.Extra,
+		SurveyId:       tokenData.SurveyId,
+		MechanismId:    tokenData.MechanismId,
+		SurveyResultId: renewSurveyToken.SurveyResultId,
+	}
+	cacheKey := fmt.Sprintf("survey-result:token:%s", token)
+	expiredAt := time.Duration(expired) * time.Second
+	// 保存到redis
+	cacheErr := cache.Instance().Put(cacheKey, renewTokenData, expiredAt)
+	if cacheErr != nil {
+		return &response.ErrCode{
+			Msg:  "数据更新失败",
+			Code: response.ERROR,
+		}
+	}
+	return nil
+}
+
+// 获取临时凭证的数据
+func GetSurveyTokenData(token string) (*validators.SurveyToken, *response.ErrCode) {
+	cacheKey := fmt.Sprintf("survey-result:token:%s", token)
+	var tokenData *validators.SurveyToken
+	tokenDataStr, cacheErr := cache.Instance().Get(cacheKey)
+	if cacheErr != nil {
+		return nil, &response.ErrCode{
+			Msg:  "无效的凭证信息",
+			Code: response.ERROR,
+		}
+	}
+	json.UnmarshalFromString(tokenDataStr, &tokenData)
+	return tokenData, nil
+}
+
+// 移除临时凭证
+func RemoveSurveyToken(token string) {
+	cacheKey := fmt.Sprintf("survey-result:token:%s", token)
+	cache.Instance().Delete(cacheKey)
+}

+ 43 - 0
service/system_setting/manage.go

@@ -0,0 +1,43 @@
+package system_setting
+
+import (
+	"surveyService/model"
+
+	"github.com/golang-module/carbon"
+	"github.com/samber/lo"
+)
+
+type SystemSetting struct {
+	Key       string `json:"key"`
+	Value     string `json:"value"`
+	CreatedAt string `json:"createdAt"`
+}
+
+// Get 批量获取
+func Get(keys []string) []*SystemSetting {
+	var systemSettings []*SystemSetting = make([]*SystemSetting, 0)
+	var settings []*model.SystemSetting
+	model.DB.Where("`key` IN ?", keys).Find(&settings)
+
+	for _, key := range keys {
+		systemSetting, has := lo.Find(settings, func(setting *model.SystemSetting) bool {
+			return setting.Key == key
+		})
+		if has {
+			systemSettings = append(systemSettings, &SystemSetting{
+				Key:       systemSetting.Key,
+				Value:     systemSetting.Value,
+				CreatedAt: carbon.Time2Carbon(systemSetting.CreatedAt).Format("Y/m/d H:i:s"),
+			})
+			continue
+		}
+		systemSettings = append(systemSettings, &SystemSetting{
+			Key:       key,
+			Value:     "",
+			CreatedAt: "",
+		})
+
+	}
+
+	return systemSettings
+}

+ 14 - 0
service/system_setting/manage_test.go

@@ -0,0 +1,14 @@
+package system_setting_test
+
+import (
+	"surveyService/service/system_setting"
+	"testing"
+)
+
+func TestGet(t *testing.T) {
+	t.Run("ok", func(t *testing.T) {
+		keys := []string{"test", "test2"}
+		systemSettings := system_setting.Get(keys)
+		t.Log(systemSettings)
+	})
+}

BIN
tests/.DS_Store


+ 23 - 0
tests/main.go

@@ -0,0 +1,23 @@
+package tests
+
+import (
+	"fmt"
+	"surveyService/cache"
+	"surveyService/model"
+
+	"github.com/joho/godotenv"
+)
+
+func Init() {
+	// 加载dotEnv环境
+	loadEnvErr := godotenv.Load("/Users/huang/Desktop/hys/surveyService/.env")
+	if loadEnvErr != nil {
+		fmt.Println("ENV环境加载Error")
+		return
+	}
+	// 开始初始化数据库
+	model.Construct(false)
+
+	// 开始初始化缓存
+	cache.InitRedis()
+}

BIN
util/.DS_Store


+ 133 - 0
util/algor/main.go

@@ -0,0 +1,133 @@
+package algor
+
+import (
+	"fmt"
+	"surveyService/response"
+	"surveyService/util"
+
+	"github.com/guonaihong/gout"
+)
+
+var s *setting
+
+type setting struct {
+	Domain    string
+	AppKey    string
+	AppSecret string
+	Debug     bool
+}
+
+func InitClient(domain, appKey, appSecret string, debug bool) {
+	s = &setting{
+		Domain:    domain,
+		AppKey:    appKey,
+		AppSecret: appSecret,
+		Debug:     debug,
+	}
+}
+
+// 获取授权列表
+func ListAuthorizes() (*BaseResponse[ListResponse[AppAuth]], *response.ErrCode) {
+	var resp *BaseResponse[ListResponse[AppAuth]]
+	resp, err := postReq[*BaseResponse[ListResponse[AppAuth]]]("/openapi/authorize/list", gout.H{})
+	if err != nil {
+		return nil, response.ErrPlatform
+	}
+	if resp.Code != 200 {
+		return nil, &response.ErrCode{
+			Code: resp.Code,
+			Msg:  resp.Message,
+		}
+	}
+	return resp, nil
+}
+
+// 获取问题模板列表
+func ListQuestionnaireTemplate() (*BaseResponse[ListResponse[QuestionnaireTemplate]], *response.ErrCode) {
+	var resp *BaseResponse[ListResponse[QuestionnaireTemplate]]
+	resp, err := postReq[*BaseResponse[ListResponse[QuestionnaireTemplate]]]("/openapi/questionnaireTemplate/list", gout.H{})
+	if err != nil {
+		return nil, response.ErrPlatform
+	}
+	if resp.Code != 200 {
+		return nil, &response.ErrCode{
+			Code: resp.Code,
+			Msg:  resp.Message,
+		}
+	}
+	return resp, nil
+}
+
+// 获取问题库列表
+func ListQuestionnaireSubject() (*BaseResponse[ListResponse[QuestionnaireSubject]], *response.ErrCode) {
+	var resp *BaseResponse[ListResponse[QuestionnaireSubject]]
+	resp, err := postReq[*BaseResponse[ListResponse[QuestionnaireSubject]]]("/openapi/questionnaireSubject/list", gout.H{})
+	if err != nil {
+		return nil, response.ErrPlatform
+	}
+	if resp.Code != 200 {
+		return nil, &response.ErrCode{
+			Code: resp.Code,
+			Msg:  resp.Message,
+		}
+	}
+	return resp, nil
+}
+
+// 获取问卷信息
+func DetailSurvey(decisionModelSn string) (*BaseResponse[DetailResponse], *response.ErrCode) {
+	var resp *BaseResponse[DetailResponse]
+	resp, err := postReq[*BaseResponse[DetailResponse]]("/openapi/survey/detail", gout.H{
+		"decisionModelSn": decisionModelSn,
+	})
+	if err != nil {
+		return nil, response.ErrPlatform
+	}
+	if resp.Code != 200 {
+		return nil, &response.ErrCode{
+			Code: resp.Code,
+			Msg:  resp.Message,
+		}
+	}
+	return resp, nil
+}
+
+// 执行决策模型
+func Run(request ExecuteRequest) (*BaseResponse[map[string]any], *response.ErrCode) {
+	var resp *BaseResponse[map[string]any]
+	resp, err := postReq[*BaseResponse[map[string]any]]("/openapi/run", gout.H{
+		"decisionModelSn": request.DecisionModelSN,
+		"type":            request.Type,
+		"surveyData":      request.SurveyData,
+		"ocrData":         request.OcrData,
+		"extra":           request.Extra,
+	})
+	if err != nil {
+		return nil, response.ErrPlatform
+	}
+	if resp.Code != 200 {
+		return nil, &response.ErrCode{
+			Code: resp.Code,
+			Msg:  resp.Message,
+		}
+	}
+	return resp, nil
+}
+
+func postReq[T any](path string, data gout.H) (T, error) {
+	url := fmt.Sprintf("%s%s", s.Domain, path)
+	fmt.Println("请求地址 ===>", url)
+	var res T
+	err := gout.POST(url).Debug(s.Debug).
+		SetHeader(gout.H{
+			"token":  fmt.Sprintf("%s:%s", s.AppKey, util.Md5(s.AppSecret)),
+			"appkey": s.AppKey,
+		}).
+		SetJSON(data).
+		BindJSON(&res).
+		Do()
+	if err != nil {
+		return res, err
+	}
+	return res, nil
+}

+ 21 - 0
util/algor/main_test.go

@@ -0,0 +1,21 @@
+package algor_test
+
+import (
+	"surveyService/util/algor"
+	"testing"
+)
+
+func init() {
+	algor.InitClient("https://api.mdfitnesscao.com", "k7bDcSj9KanD8TB4", "96fcO7IxM1pKYjGgl0D4pbxRr98PeShU", true)
+}
+
+func TestGetAuthorizeList(t *testing.T) {
+	t.Run("test", func(t *testing.T) {
+		response, err := algor.ListAuthorizes()
+		list := response.Data.List
+		if err != nil {
+			t.Errorf("err: %v", err)
+		}
+		t.Logf("list len: %d", len(list))
+	})
+}

+ 70 - 0
util/algor/structs.go

@@ -0,0 +1,70 @@
+package algor
+
+type BaseResponse[T any] struct {
+	Code    int    `json:"code"`
+	Data    T      `json:"data"`
+	Message string `json:"message"`
+	Success bool   `json:"success"`
+}
+
+type ListResponse[T any] struct {
+	List []T `json:"list"`
+}
+
+type DetailResponse struct {
+	Detail map[string]any `json:"detail"`
+}
+
+type AppAuth struct {
+	ID              int64          `json:"id" form:"id"`
+	AppId           int64          `json:"appId" form:"appId"`
+	DecisionModelId int64          `json:"decisionModelId" form:"decisionModelId"`
+	DecisionModel   *DecisionModel `json:"decisionModel" form:"decisionModel"`
+	Remark          string         `json:"remark" form:"remark"`
+	UsedTotal       int            `json:"usedTotal" form:"usedTotal"`
+	Status          int            `json:"status" form:"status"`
+	CreatedAt       string         `json:"createdAt"`
+}
+type DecisionModel struct {
+	ID        int64  `json:"id" form:"id"`
+	Name      string `json:"name" form:"name"`
+	SN        string `json:"sn" form:"sn"`
+	CreatedAt string `json:"createdAt" form:"-"`
+}
+
+type ExecuteRequest struct {
+	Type            int            `json:"type" form:"type"`                       // 处理类型,1:问卷,2:ocr
+	SurveyData      map[string]any `json:"surveyData" form:"surveyData"`           // 问卷数据
+	OcrData         *OCRDataList   `json:"ocrData" form:"ocrData"`                 // ocr数据
+	DecisionModelSN string         `json:"decisionModelSn" form:"decisionModelSn"` // 决策模型编号
+	Extra           string         `json:"extra" form:"extra"`                     // 额外数据
+}
+
+type OCRDataList struct {
+	UserName string           `json:"userName"` //用户姓名
+	UserAge  int              `json:"userAge"`  //用户年龄
+	UserSex  string           `json:"userSex"`  //用户性别
+	OCRData  []map[string]any `json:"ocrData"`  //OCR数据
+}
+
+// 问题库
+type QuestionnaireSubject struct {
+	ID        int64  `json:"id" form:"id"`
+	SN        string `json:"sn" form:"sn"`
+	Type      int    `json:"type" form:"type"`
+	Title     string `json:"title" form:"title"`
+	Validator string `json:"validator" form:"validator"`
+	Remark    string `json:"remark" form:"remark"`
+	Mark      string `json:"mark" form:"mark"`
+	CreatedAt string `json:"createdAt" form:"-"`
+}
+
+// 问题模板
+type QuestionnaireTemplate struct {
+	ID           int64  `json:"id" form:"id"`
+	SN           string `json:"sn" form:"sn"`
+	Title        string `json:"title" form:"title"`
+	Peg          string `json:"peg" form:"peg"`
+	SubjectTotal int64  `json:"subjectTotal" form:"-"` // 关联的题目数
+	CreatedAt    string `json:"createdAt" form:"-"`
+}

+ 51 - 0
util/aliyun/dysms.go

@@ -0,0 +1,51 @@
+package aliyun
+
+import (
+	"fmt"
+	"os"
+
+	openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
+	dysmsapi20170525 "github.com/alibabacloud-go/dysmsapi-20170525/v3/client"
+	util "github.com/alibabacloud-go/tea-utils/v2/service"
+	"github.com/alibabacloud-go/tea/tea"
+)
+
+func Init() (*dysmsapi20170525.Client, error) {
+	AccessKeyId := os.Getenv("ALIYUN_ACCESS_KEY_ID")
+	AccessKeySecret := os.Getenv("ALIYUN_ACCESS_KEY_SECRET")
+	config := &openapi.Config{
+		// 您的 AccessKey ID
+		AccessKeyId: &AccessKeyId,
+		// 您的 AccessKey Secret
+		AccessKeySecret: &AccessKeySecret,
+	}
+	// 访问的域名
+	config.Endpoint = tea.String("dysmsapi.aliyuncs.com")
+	client := &dysmsapi20170525.Client{}
+	client, err := dysmsapi20170525.NewClient(config)
+	if err != nil {
+		return nil, err
+	}
+
+	return client, nil
+}
+
+// 发送短信验证码
+func SendCaptcha(mobile string, code string) error {
+	sendSmsRequest := &dysmsapi20170525.SendSmsRequest{
+		PhoneNumbers:  &mobile,
+		SignName:      tea.String("上医未来"),
+		TemplateCode:  tea.String("SMS_243630961"),
+		TemplateParam: tea.String(fmt.Sprintf("{\"code\":\"%s\"}", code)),
+	}
+	client, err := Init()
+	if err != nil {
+		return err
+	}
+	runtime := &util.RuntimeOptions{}
+	_, err = client.SendSmsWithOptions(sendSmsRequest, runtime)
+	if err != nil {
+		return err
+	}
+	return nil
+}

+ 11 - 0
util/constants/constants.go

@@ -0,0 +1,11 @@
+package constants
+
+const (
+	UserCacheKey             = "user" // 用户登录的缓存键
+	MechanismCacheKey        = "mechanism"
+	MemberCacheKey           = "member"            // 用户登录的缓存键
+	MemberMechanismIDKey     = "memberMechanismId" // 用户H5的机构来源
+	MechanismOpenAPICacheKey = "mechanismOpenApi"  // 开放的机构API
+	SurveyTokenDataCacheKey  = "surveyTokenData"   // 问卷token
+	SurveyTokenCacheKey      = "surveyToken"       // 问卷token
+)

+ 1 - 0
util/log/log.go

@@ -0,0 +1 @@
+package log

+ 73 - 0
util/rabbitmq/index.go

@@ -0,0 +1,73 @@
+package rabbitmq
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"time"
+
+	jsoniter "github.com/json-iterator/go"
+	"github.com/streadway/amqp"
+)
+
+type WebhookMessage struct {
+	Body   any `json:"body"`
+	Config struct {
+		Url string `json:"url"`
+	} `json:"config"`
+}
+
+var json = jsoniter.ConfigCompatibleWithStandardLibrary
+
+func Webhook(webhookMessage WebhookMessage) error {
+	conn, err := Init()
+	if err != nil {
+		return err
+	}
+	p, err := conn.Producer(&Exchange{
+		Name: os.Getenv("RABBITMQ_WEBHOOK_EXCHANGE"),
+		Kind: amqp.ExchangeDirect,
+		Key:  os.Getenv("RABBITMQ_WEBHOOK_ROUTING"),
+	}, nil)
+	if err != nil {
+		return err
+	}
+
+	message, err := json.MarshalToString(webhookMessage)
+	if err != nil {
+		return err
+	}
+	fmt.Println("message", message)
+
+	err = p.PublishExchange(amqp.Publishing{
+		ContentType:  "text/plain",
+		DeliveryMode: amqp.Persistent,
+		Body:         []byte(message),
+	})
+	if err != nil {
+		return err
+	}
+	// 关闭连接
+	defer conn.conn.Close()
+	return nil
+}
+
+func Init() (*Conn, error) {
+	var broker2 = Broker{
+		Ssl:       false,                          // bool
+		Username:  os.Getenv("RABBITMQ_USER"),     // string
+		Password:  os.Getenv("RABBITMQ_PASSWORD"), // string
+		Server:    os.Getenv("RABBITMQ_HOST"),     // string
+		Port:      os.Getenv("RABBITMQ_PORT"),     // string
+		Vhost:     os.Getenv("RABBITMQ_VHOST"),    // string
+		TSL:       nil,                            // *tls.Config
+		ProxyAddr: "",                             // string
+		Beatime:   15 * time.Second,               // time.Duration
+	}
+
+	conn, err := New(context.Background(), broker2)
+	if err != nil {
+		return nil, err
+	}
+	return conn, nil
+}

+ 5 - 0
util/rabbitmq/logger.go

@@ -0,0 +1,5 @@
+package rabbitmq
+
+type Logger interface {
+	Errorf(format string, args ...interface{})
+}

+ 103 - 0
util/rabbitmq/producer.go

@@ -0,0 +1,103 @@
+package rabbitmq
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"os"
+	"time"
+
+	"github.com/streadway/amqp"
+)
+
+var p *Producer
+
+type Producer struct {
+	ctx         context.Context
+	msgIDPrefix string
+	exchange    *Exchange
+	ch          *amqp.Channel
+	conn        *Conn
+	logger      Logger
+	queue       *Queue
+}
+
+func Publish(data string) error {
+	conn, err := New(context.Background(), Broker{
+		Ssl:       false,                          // bool
+		Username:  os.Getenv("RABBITMQ_USER"),     // string
+		Password:  os.Getenv("RABBITMQ_PASSWORD"), // string
+		Server:    os.Getenv("RABBITMQ_HOST"),     // string
+		Port:      os.Getenv("RABBITMQ_PORT"),     // string
+		Vhost:     os.Getenv("RABBITMQ_VHOST"),    // string
+		TSL:       nil,                            // *tls.Config
+		ProxyAddr: "",                             // string
+		Beatime:   15 * time.Second,               // time.Duration
+	})
+	if err != nil {
+		panic(err)
+	}
+	var exchange *Exchange
+	var queue *Queue
+	if os.Getenv("RABBITMQ_PDF_BUILDER_QUEUE") != "" {
+		queue = &Queue{
+			Name:       os.Getenv("RABBITMQ_PDF_BUILDER_QUEUE"),
+			Durable:    true,
+			AutoDelete: true,
+		}
+	}
+	if exchange != nil || queue != nil {
+		_, err = conn.Producer(exchange, queue)
+		if err != nil {
+			panic(err)
+		}
+	}
+
+	if p == nil {
+		return errors.New("nil producer")
+	}
+	// 关闭连接
+	defer conn.conn.Close()
+	return p.Publish(amqp.Publishing{
+		ContentType:  "text/plain",
+		DeliveryMode: amqp.Persistent,
+		Body:         []byte(data),
+	})
+}
+
+func PublishExchange(data string) error {
+	if p == nil {
+		return errors.New("nil producer")
+	}
+	return p.PublishExchange(amqp.Publishing{
+		ContentType:  "text/plain",
+		DeliveryMode: amqp.Persistent,
+		Body:         []byte(data),
+	})
+}
+
+func (p *Producer) Conn() *Conn {
+	return p.conn
+}
+
+func (p *Producer) Publish(msg amqp.Publishing) error {
+	msg.MessageId = fmt.Sprintf("%s:%d", p.msgIDPrefix, p.conn.MsgId())
+	return p.ch.Publish(
+		"",
+		p.queue.Name, // routing key
+		false,        // mandatory
+		false,        // immediate
+		msg,
+	)
+}
+
+func (p *Producer) PublishExchange(msg amqp.Publishing) error {
+	msg.MessageId = fmt.Sprintf("%s:%d", p.msgIDPrefix, p.conn.MsgId())
+	return p.ch.Publish(
+		p.exchange.Name, // exchange
+		p.exchange.Key,  // routing key
+		false,           // mandatory
+		false,           // immediate
+		msg,
+	)
+}

+ 177 - 0
util/rabbitmq/rabbitmq.go

@@ -0,0 +1,177 @@
+package rabbitmq
+
+import (
+	"context"
+	"crypto/tls"
+	"net"
+	"strconv"
+	"strings"
+	"sync"
+	"sync/atomic"
+	"time"
+
+	"github.com/streadway/amqp"
+)
+
+type Broker struct {
+	Ssl       bool
+	Username  string
+	Password  string
+	Server    string
+	Port      string
+	Vhost     string
+	TSL       *tls.Config
+	ProxyAddr string
+	Beatime   time.Duration
+}
+
+type Conn struct {
+	ctx    context.Context
+	msgID  uint64
+	broker Broker
+	logger Logger
+	l      sync.Mutex
+	conn   *amqp.Connection
+}
+
+type Exchange struct {
+	Key        string     // 生产者routingKey, 消费者bindingKey
+	Name       string     // 交换机名称
+	Kind       string     // fanout(广播)	direct(直接交换)比fanout多加了一层密码限制(routingKey)	topic(主题)	headers(首部)
+	Durable    bool       // 是否持久化 建议true, RabbitMQ关闭后,没有持久化的Exchange将被清除
+	AutoDelete bool       // 是否自动删除 建议false, 如果没有与之绑定的Queue,直接删除
+	Internal   bool       // 是否内置的 建议false,如果为true,只能通过Exchange到Exchange
+	Delay      bool       // 是否是延迟队列
+	NoWait     bool       // 是否非阻塞 建议false, true表示阻塞,创建交换器的请求发送后,阻塞等待RMQ Server返回信息。false非阻塞:不会阻塞等待RMQ Server的返回信息,而RMQ Server也不会返回信息。(不推荐使用)
+	Args       amqp.Table // 其他参数, 死信就是通过该参数设置
+}
+
+type Queue struct {
+	Name          string
+	Durable       bool       // 是否持久化 建议true, RabbitMQ关闭后,没有持久化的Exchange将被清除
+	AutoDelete    bool       // 是否自动删除 建议false, 如果没有与之绑定的Queue,直接删除
+	Exclusive     bool       // 是否排外的 建议false, 当连接关闭时connection.close()该队列是否会自动删除。	该队列是否是私有的private,如果不是排外的,可以使用两个消费者都访问同一个队列,没有任何问题,如果是排外的,会对当前队列加锁,其他通道channel是不能访问的,如果强制访问会报异常:com.rabbitmq.client.ShutdownSignalException: channel error; protocol method: #method(reply-code=405, reply-text=RESOURCE_LOCKED - cannot obtain exclusive access to locked queue 'queue_name' in vhost '/', class-id=50, method-id=20)。	应用于一个队列只能有一个消费者来消费的场景。
+	NoWait        bool       // 是否非阻塞 建议false, true表示阻塞,创建交换器的请求发送后,阻塞等待RMQ Server返回信息。false非阻塞:不会阻塞等待RMQ Server的返回信息,而RMQ Server也不会返回信息。(不推荐使用)
+	Args          amqp.Table // 其他参数, 延迟就是通过该参数设置
+	PrefetchCount int        // 会告诉RabbitMQ不要同时给一个消费者推送多于N个消息,即一旦有N个消息还没有ack,则该Consumer将block掉,直到有消息ack
+	PrefetchSize  int        // prefetchSize:最多传输的内容的大小的限制,0 为不限制,但据说prefetchSize参数,rabbitmq没有实现
+	Global        bool       // 是否将上面设置应用于channel,简单点说,就是上面限制是channel级别的还是Consumer级别)
+	AutoAck       bool       // 自动确认
+}
+
+func (broker Broker) DSN() string {
+	protocol := "amqp"
+	if broker.Ssl {
+		protocol = "amqps"
+	}
+	builder := strings.Builder{}
+	builder.WriteString(protocol)
+	builder.WriteString("://")
+	if broker.Username != `` {
+		builder.WriteString(broker.Username)
+		builder.WriteString(":")
+		builder.WriteString(broker.Password)
+		builder.WriteString("@")
+	}
+	builder.WriteString(broker.Server)
+	if broker.Port != `` {
+		builder.WriteString(":")
+		builder.WriteString(broker.Port)
+	}
+	builder.WriteString("/")
+	if broker.Vhost != `` {
+		builder.WriteString(broker.Vhost)
+	}
+	return builder.String()
+}
+
+func New(ctx context.Context, broker Broker) (*Conn, error) {
+	c := &Conn{
+		ctx:    ctx,
+		broker: broker,
+	}
+	//尝试连接
+	if err := c.connect(); err != nil {
+		return nil, err
+	}
+
+	return c, nil
+}
+
+// Producer 生成一个生产者
+func (c *Conn) Producer(exchange *Exchange, queue *Queue) (*Producer, error) {
+	p = &Producer{
+		ctx:         c.ctx,
+		msgIDPrefix: strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10),
+		exchange:    exchange,
+		conn:        c,
+		logger:      c.logger,
+		queue:       queue,
+	}
+	ch, err := c.channel(exchange, queue)
+	if err != nil {
+		return nil, err
+	}
+	p.ch = ch
+	return p, nil
+}
+
+func (c *Conn) MsgId() uint64 {
+	return atomic.AddUint64(&c.msgID, 1)
+}
+
+// 初始化channel
+func (c *Conn) channel(exchange *Exchange, queue *Queue) (*amqp.Channel, error) {
+	ch, err := c.conn.Channel()
+	if err != nil {
+		return nil, err
+	}
+
+	if exchange != nil {
+		err := ch.ExchangeDeclarePassive(exchange.Name, exchange.Kind, exchange.Durable, exchange.AutoDelete, exchange.Internal, exchange.NoWait, nil)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	if queue != nil {
+		_, err = ch.QueueDeclarePassive(
+			queue.Name,
+			queue.Durable,    // durable
+			queue.AutoDelete, // auto_delete
+			queue.Exclusive,  // exclusive
+			queue.NoWait,     // no_wait
+			nil,
+		)
+	}
+	if err != nil {
+		return nil, err
+	}
+	return ch, nil
+}
+
+func (c *Conn) connect() error {
+	c.l.Lock()
+	if c.conn != nil && !c.conn.IsClosed() {
+		c.l.Unlock()
+		return nil
+	}
+	var err error
+	if c.broker.TSL != nil {
+		c.conn, err = amqp.DialTLS(c.broker.DSN(), c.broker.TSL)
+	} else if c.broker.ProxyAddr != `` {
+		c.conn, err = amqp.DialConfig(c.broker.DSN(), amqp.Config{
+			Dial: func(network, addr string) (net.Conn, error) {
+				return net.Dial("tcp", c.broker.ProxyAddr)
+			},
+		})
+	} else {
+		c.conn, err = amqp.Dial(c.broker.DSN())
+	}
+	if err == nil {
+		c.conn.Config.Heartbeat = c.broker.Beatime
+		c.conn.Config.Locale = "en_US"
+	}
+	c.l.Unlock()
+	return err
+}

+ 82 - 0
util/rabbitmq/rabbitmq_test.go

@@ -0,0 +1,82 @@
+package rabbitmq_test
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"surveyService/util/rabbitmq"
+
+	"github.com/streadway/amqp"
+)
+
+var broker2 = rabbitmq.Broker{
+	Ssl:       false,            // bool
+	Username:  "md",             // string
+	Password:  "fNaxiyVthc",     // string
+	Server:    "47.109.41.4",    // string
+	Port:      "5672",           // string
+	Vhost:     "drmd",           // string
+	TSL:       nil,              // *tls.Config
+	ProxyAddr: "",               // string
+	Beatime:   15 * time.Second, // time.Duration
+}
+
+func TestRabbitmq(t *testing.T) {
+	conn, err := rabbitmq.New(context.Background(), broker2)
+	if err != nil {
+		t.Fatal(err)
+	}
+	p, err := conn.Producer(nil, &rabbitmq.Queue{
+		Name:       "queue:dev:pdf:builder",
+		Durable:    true,
+		AutoDelete: true,
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = p.Publish(amqp.Publishing{
+		ContentType:  "text/plain",
+		DeliveryMode: amqp.Persistent,
+		Body:         []byte("hello"),
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = p.Publish(amqp.Publishing{
+		ContentType:  "text/plain",
+		DeliveryMode: amqp.Persistent,
+		Body:         []byte("world"),
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	time.Sleep(time.Second * 1)
+}
+
+func TestRabbitmqDelay(t *testing.T) {
+	conn, err := rabbitmq.New(context.Background(), broker2)
+	if err != nil {
+		t.Fatal(err)
+	}
+	p, err := conn.Producer(&rabbitmq.Exchange{
+		Name: "queue:webhook:exchange",
+		Kind: amqp.ExchangeDirect,
+		Key:  "queue:webhook:routing",
+	}, nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = p.PublishExchange(amqp.Publishing{
+		Headers: amqp.Table{
+			"x-delay": int64(10000),
+		},
+		ContentType:  "text/plain",
+		DeliveryMode: amqp.Persistent,
+		Body:         []byte("hello world"),
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	time.Sleep(time.Second * 1)
+}

+ 238 - 0
util/util.go

@@ -0,0 +1,238 @@
+package util
+
+import (
+	"bytes"
+	"crypto/md5"
+	"crypto/rand"
+	"database/sql"
+	"encoding/hex"
+	"fmt"
+	"image"
+	"io/ioutil"
+	"math/big"
+	"net/http"
+	"regexp"
+	"strings"
+	"time"
+
+	"surveyService/model/types"
+
+	"github.com/gin-gonic/gin"
+	jsoniter "github.com/json-iterator/go"
+	"github.com/speps/go-hashids/v2"
+)
+
+// php序列化的对象转map之后,key值会有特定的字节前缀导致无法解析,需要去掉
+func PhpMap2Go(in map[string]interface{}) map[string]interface{} {
+	result := make(map[string]interface{})
+	for i, v := range in {
+		tmpi := []byte(i)
+		if tmpi[0] == 0 && tmpi[1] == 42 && tmpi[2] == 0 {
+			result[string(tmpi[3:])] = v
+		} else {
+			result[i] = v
+		}
+	}
+	return result
+}
+
+func GetRequestParam(c *gin.Context, key string) string {
+	if c.Request.Method == http.MethodGet {
+		return c.Query(key)
+	} else {
+		// 如果是Json格式
+		if c.Request.Header.Get("Content-Type") == "application/json" {
+			JsonData := GetRequestJsonData(c)
+			// 判断类型
+			if _, ok := JsonData[key].(string); !ok {
+				return ""
+			}
+			return JsonData[key].(string)
+		}
+		return c.PostForm(key)
+	}
+}
+
+func GetRequestJsonData(c *gin.Context) map[string]any {
+	data, _ := c.GetRawData()
+	var body map[string]any
+	_ = json.Unmarshal(data, &body)
+	c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(data))
+	return body
+}
+
+func GetPostFormParams(c *gin.Context) (map[string]any, error) {
+	var postMap = make(map[string]any, len(c.Request.PostForm))
+	for k, v := range c.Request.PostForm {
+		if len(v) > 1 {
+			postMap[k] = v
+		} else if len(v) == 1 {
+			postMap[k] = v[0]
+		}
+	}
+
+	return postMap, nil
+}
+
+// 将ID编译为HashId
+func GetHashId(id int64, salt string) string {
+	hd := hashids.NewData()
+	hd.Salt = salt
+	hd.MinLength = 8
+	h, _ := hashids.NewWithData(hd)
+	hashId, _ := h.Encode([]int{int(id)})
+	return hashId
+}
+
+// 根据HashId获取真实ID
+func GetIdByHashId(hashId string, salt string) (int64, error) {
+	hd := hashids.NewData()
+	hd.Salt = salt
+	hd.MinLength = 8
+	h, _ := hashids.NewWithData(hd)
+	d, err := h.DecodeWithError(hashId)
+	if err != nil {
+		return 0, err
+	}
+	return int64(d[0]), nil
+}
+
+func GetFromGinContext[T any](ctx *gin.Context, key string) (T, bool) {
+	var some T
+	value, exist := ctx.Get(key)
+	if !exist {
+		return some, false
+	}
+	some, ok := value.(T)
+	if !ok {
+		return some, false
+	}
+	return some, true
+}
+
+// MD5加密
+func Md5(src string) string {
+	m := md5.New()
+	m.Write([]byte(src))
+	res := hex.EncodeToString(m.Sum(nil))
+	return res
+}
+
+// 获取随机数
+func GetRandomNumber() string {
+	result, _ := rand.Int(rand.Reader, big.NewInt(9999))
+	return fmt.Sprintf("%04d", result)
+}
+
+// 转为sql.NullTime
+func ToNullTime(t time.Time) types.NullTime {
+	return types.NullTime{NullTime: sql.NullTime{Time: t, Valid: !t.IsZero()}}
+}
+
+// 数据脱敏
+func HideStar(str string) (result string) {
+	if str == "" {
+		return ""
+	}
+	if strings.Contains(str, "@") { // 邮箱
+		res := strings.Split(str, "@")
+		if len(res[0]) < 3 {
+			resString := "***"
+			result = resString + "@" + res[1]
+		} else {
+			res2 := Substr2(str, 0, 3)
+			resString := res2 + "***"
+			result = resString + "@" + res[1]
+		}
+		return result
+	} else {
+		reg := `^1[0-9]\d{9}$`
+		rgx := regexp.MustCompile(reg)
+		mobileMatch := rgx.MatchString(str)
+		if mobileMatch { // 手机号
+			result = Substr2(str, 0, 3) + "****" + Substr2(str, 7, 11)
+		} else {
+			nameRune := []rune(str)
+			lens := len(nameRune)
+			if lens <= 1 {
+				result = "***"
+			} else if lens == 2 {
+				result = string(nameRune[:1]) + "*"
+			} else if lens == 3 {
+				result = string(nameRune[:1]) + "*" + string(nameRune[2:3])
+			} else if lens == 4 {
+				result = string(nameRune[:1]) + "**" + string(nameRune[lens-1:lens])
+			} else if lens > 4 {
+				result = string(nameRune[:2]) + "***" + string(nameRune[lens-2:lens])
+			}
+		}
+		return
+	}
+}
+func Substr2(str string, start int, end int) string {
+	rs := []rune(str)
+	return string(rs[start:end])
+}
+
+// 读取远程文件
+func ReadImageData(url string) image.Image {
+	resp, err := http.Get(url)
+	if err != nil {
+		return nil
+	}
+	defer resp.Body.Close()
+	img, _, err := image.Decode(resp.Body)
+	if err != nil {
+		return nil
+	}
+	return img
+}
+
+// 获取随机字符串
+func RandString(n int) string {
+	var longLetters = []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_")
+	if n <= 0 {
+		return ""
+	}
+	b := make([]byte, n)
+	arc := uint8(0)
+	if _, err := rand.Read(b[:]); err != nil {
+		return ""
+	}
+	for i, x := range b {
+		arc = x & 62
+		b[i] = longLetters[arc]
+	}
+	return string(b)
+}
+
+var json = jsoniter.ConfigCompatibleWithStandardLibrary
+
+func JsonEncode(v interface{}) string {
+	str, _ := json.MarshalToString(v)
+	return str
+}
+
+func Println(v interface{}) string {
+	var str string
+	// 判断是否是string类型
+	str = JsonEncode(str)
+	return str
+}
+
+func InArrayString(val string, arr []string) bool {
+	for _, v := range arr {
+		if v == val {
+			return true
+		}
+	}
+	return false
+}
+
+// 手机号
+func RegexMobile(mobile string) bool {
+	if matched, _ := regexp.MatchString(`^1[3456789]\d{9}$`, mobile); matched {
+		return true
+	}
+	return false
+}

+ 56 - 0
util/validator/validator.go

@@ -0,0 +1,56 @@
+package validator
+
+//将验证器错误翻译成中文
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/gin-gonic/gin/binding"
+	"github.com/go-playground/locales/zh"
+	ut "github.com/go-playground/universal-translator"
+	"github.com/go-playground/validator/v10"
+	zh_translations "github.com/go-playground/validator/v10/translations/zh"
+)
+
+var (
+	uni *ut.UniversalTranslator
+	// validate *validator.Validate
+	trans ut.Translator
+)
+
+func Init() {
+	//注册翻译器
+	zh := zh.New()
+	uni = ut.New(zh, zh)
+
+	trans, _ = uni.GetTranslator("zh")
+
+	//获取gin的校验器
+	validate := binding.Validator.Engine().(*validator.Validate)
+	//注册翻译器
+	zh_translations.RegisterDefaultTranslations(validate, trans)
+}
+
+// Translate 翻译错误信息
+func Translate(err error) map[string][]string {
+
+	var result = make(map[string][]string)
+
+	errors := err.(validator.ValidationErrors)
+
+	for _, err := range errors {
+		result[err.Field()] = append(result[err.Field()], err.Translate(trans))
+	}
+	return result
+}
+
+func TranslateError(err error) error {
+	var validationErr validator.ValidationErrors
+	if errors.As(err, &validationErr) {
+		errs := err.(validator.ValidationErrors)
+		return errors.New(errs[0].Translate(trans))
+	}
+	fmt.Println("数据格式校验错误: ", err)
+	return errors.New("数据格式校验不通过" + err.Error())
+}

+ 63 - 0
validators/questionnaire.go

@@ -0,0 +1,63 @@
+package validators
+
+// 问题库
+type QuestionnaireSubject struct {
+	ID        int64  `json:"id" form:"id" binding:"omitempty"`
+	SN        string `json:"sn" form:"sn" binding:"required_if=ID 0,omitempty,alphanum,min=1,max=255"`
+	Type      int    `json:"type" form:"type" binding:"required,number"`
+	Title     string `json:"title" form:"title" binding:"required,min=1,max=255"`
+	Validator string `json:"validator" form:"validator" binding:"omitempty"`
+	Remark    string `json:"remark" form:"remark" binding:"omitempty,min=1,max=255"`
+	Mark      string `json:"mark" form:"mark" binding:"omitempty,min=1,max=255"`
+	CreatedAt string `json:"createdAt" form:"-" binding:"-"`
+}
+
+// 问题模板
+type QuestionnaireTemplate struct {
+	ID           int64                   `json:"id" form:"id" binding:"omitempty,number"`
+	SN           string                  `json:"sn" form:"sn" binding:"required_if=ID 0,omitempty,alphanum,min=1,max=255"`
+	Title        string                  `json:"title" form:"title" binding:"required,min=1,max=255"`
+	Peg          string                  `json:"peg" form:"peg" binding:"omitempty"`
+	SubjectTotal int                     `json:"subjectTotal" form:"-" binding:"-"` // 关联的题目数
+	SubjectIds   []string                `json:"subjectIds" form:"-" binding:"-"`
+	Subjects     []*QuestionnaireSubject `json:"subjects" form:"-"` // 关联的题目列表
+	CreatedAt    string                  `json:"createdAt" form:"-" binding:"-"`
+}
+
+// 问题模板关联的问题库
+type QuestionnaireTemplateSubject struct {
+	ID                   int64                 `json:"id" form:"-" binding:"-"`
+	Sort                 int64                 `json:"sort" form:"-" binding:"-"`
+	QuestionnaireSubject *QuestionnaireSubject `json:"questionnaireSubject" form:"-" binding:"-"`
+}
+
+// 问卷
+type QuestionnaireSurvey struct {
+	ID             int64                         `json:"id" form:"id" binding:"omitempty,number"`
+	Title          string                        `json:"title" form:"title" binding:"required,min=1,max=255"`
+	SN             string                        `json:"sn" form:"sn" binding:"required_if=ID 0,omitempty,alphanum,min=1,max=255"`
+	CanSkipIntro   int                           `json:"canSkipIntro" form:"canSkipIntro" binding:"required,number,oneof=1 2"`
+	CanSkipResult  int                           `json:"canSkipResult" form:"canSkipResult" binding:"required,number,oneof=1 2"`
+	GuestAvatar    string                        `json:"guestAvatar" form:"guestAvatar" binding:"omitempty"`
+	CustomerAvatar string                        `json:"customerAvatar" form:"customerAvatar" binding:"omitempty"`
+	GreetingText   string                        `json:"greetingText" form:"greetingText" binding:"omitempty"`
+	FinishedText   string                        `json:"finishedText" form:"finishedText" binding:"omitempty"`
+	Intro          string                        `json:"intro" form:"intro" binding:"omitempty"`
+	Status         int                           `json:"status" form:"status" binding:"omitempty,number,oneof=1 2"`
+	Returns        int                           `json:"returns" form:"-" binding:"-"`
+	Dsl            string                        `json:"dsl" form:"dsl" binding:"omitempty"`
+	Type           int                           `json:"type" form:"type" binding:"omitempty,number,oneof=1"`
+	Peg            string                        `json:"peg" form:"peg" binding:"omitempty"`
+	Remark         string                        `json:"remark" form:"remark" binding:"omitempty"`
+	Subjects       []*SurveyQuestionnaireSubject `json:"subjects" form:"-" binding:"-"`
+	CreatedAt      string                        `json:"createdAt" form:"-" binding:"-"`
+	UpdatedAt      string                        `json:"updatedAt" form:"-" binding:"-"`
+}
+
+// 问题模板关联的问题库
+type SurveyQuestionnaireSubject struct {
+	ID                   int64                 `json:"id" form:"-" binding:"-"`
+	Sort                 int64                 `json:"sort" form:"-" binding:"-"`
+	IsRequired           bool                  `json:"isRequired" form:"-" binding:"-"`
+	QuestionnaireSubject *QuestionnaireSubject `json:"questionnaireSubject" form:"-" binding:"-"`
+}

+ 67 - 0
validators/survey.go

@@ -0,0 +1,67 @@
+package validators
+
+import "gogs.uu.mdfitnesscao.com/hys/sdk"
+
+type Survey struct {
+	ID         string `json:"id" form:"id" binding:"omitempty"`
+	Name       string `json:"name" form:"name" binding:"required,min=1,max=255"`
+	SurveyCode string `json:"surveyCode" form:"surveyCode" binding:"required"`
+	Cover      string `json:"cover" form:"cover" binding:"omitempty,url"`
+	Status     int    `json:"status" form:"-" binding:"-"`
+	Remark     string `json:"remark" form:"-" binding:"-"`
+	Type       int64  `json:"type" form:"type" binding:"required"`
+	CreatedAt  string `json:"createdAt" form:"-" binding:"-"`
+	UpdatedAt  string `json:"updatedAt" form:"-" binding:"-"`
+}
+
+type SurveyMechanism struct {
+	ID                 string         `json:"id" form:"id" binding:"required"`
+	AuthorizeStatus    int            `json:"authorizeStatus" form:"-" binding:"-"`
+	Status             int            `json:"status" form:"-" binding:"-"`
+	MechanismId        string         `json:"mechanismId" form:"mechanismId" binding:"-"`
+	Mechanism          *sdk.Mechanism `json:"mechanism" form:"-" binding:"-"`
+	SurveyId           string         `json:"surveyId" form:"surveyId" binding:"-"`
+	Survey             *Survey        `json:"survey" form:"-" binding:"-"`
+	Permissions        string         `json:"permissions" form:"-" binding:"-"`
+	Name               string         `json:"name" form:"name" binding:"required,min=1,max=255"`
+	Cover              string         `json:"cover" form:"cover" binding:"omitempty,url"`
+	Description        string         `json:"description" form:"description" binding:"omitempty"`
+	SubTitle           string         `json:"subTitle" form:"subTitle" binding:"omitempty"`
+	LastVisitTime      string         `json:"lastVisitTime" form:"-" binding:"-"`      // 用户最后一次访问时间
+	LastSurveyResultId string         `json:"lastSurveyResultId" form:"-" binding:"-"` // 用户最后一次答题结果ID
+	CreatedAt          string         `json:"createdAt" form:"-" binding:"-"`
+	UpdatedAt          string         `json:"updatedAt" form:"-" binding:"-"`
+}
+
+type SurveyResult struct {
+	ID                string           `json:"id" form:"id" binding:"omitempty"`                              // 问卷结果编号(字符串)
+	Method            int              `json:"method" form:"method" binding:"-"`                              // 答题方式
+	StartTime         string           `json:"startTime" form:"startTime" binding:"required"`                 // 开始时间
+	EndTime           string           `json:"endTime" form:"endTime" binding:"required"`                     // 结束时间
+	ArchivesId        string           `json:"archivesId" form:"archivesId" binding:"required"`               // 档案ID
+	Status            int              `json:"status" form:"-" binding:"-"`                                   // 状态
+	Remark            string           `json:"remark" form:"-" binding:"-"`                                   // 备注
+	AnswerResult      map[string]any   `json:"answerResult" form:"-" binding:"-"`                             // 答题结果
+	ResultRaw         string           `json:"resultRaw" form:"-" binding:"-"`                                // 结果原始数据
+	SurveyMechanismId string           `json:"surveyMechanismId" form:"surveyMechanismId" binding:"required"` // 机构问卷ID
+	SurveyMechanism   *SurveyMechanism `json:"surveyMechanism" form:"-" binding:"-"`                          // 机构问卷信息
+	CanAnalyze        bool             `json:"canAnalyze" form:"-" binding:"-"`                               // 是否可以分析
+	CreatedAt         string           `json:"createdAt" form:"-" binding:"-"`
+	UpdatedAt         string           `json:"updatedAt" form:"-" binding:"-"`
+}
+
+type SurveyResultPaginate struct {
+	Page              int    `json:"page" form:"page" binding:"omitempty,min=1"`
+	PageSize          int    `json:"pageSize" form:"pageSize" binding:"omitempty,min=1,max=100"`
+	Method            int    `json:"method" form:"method" binding:"omitempty"`
+	SurveyMechanismId string `json:"surveyMechanismId" form:"surveyMechanismId" binding:"omitempty"`
+	SurveyId          string `json:"surveyId" form:"surveyId" binding:"omitempty"`
+	ArchivesId        string `json:"archivesId" form:"archivesId" binding:"omitempty"`
+}
+
+type SurveyToken struct {
+	Extra          string `json:"extra"`          // 第三方自定义参数
+	SurveyId       string `json:"surveyId"`       // 问卷编号
+	MechanismId    string `json:"mechanismId"`    // 机构编号
+	SurveyResultId string `json:"surveyResultId"` // 问卷结果编号
+}