第一篇
1.会议照片
2.各个成员在 Alpha 阶段认领的任务
成员 |
任务 |
蔡鑫源 |
完成博客随笔、UI设计 |
纪培浩 |
设计数据库和编写API接口 |
罗汉光 |
界面开发、android |
拜合提亚尔 |
调试、测试 |
3.明日各个成员的任务安排
成员 |
任务 |
蔡鑫源 |
对APP的界面进行美化 |
纪培浩 |
数据库设计 |
罗汉光 |
定义各功能模块接口 |
拜合提亚尔 |
制定组装测试计划 |
4.整个项目预期的任务量
APP的大概界面构思和设计,大功能模块代码编写。正式进入产品的原型设计阶段。UI、UE开始设计,形成初步的效果图。在经过确认后界面的效果图正式设计完成。产品在设计图完成后,进入研发阶段。通过编程语言形成正式的程序。至此,App的制作过程就完成了一大部分,可以进入测试部进行测试。
5.敏捷开发前的感想
对于需要多人合作的项目而言,一套科学的开发流程尤为重要。在很多情况下,单靠个人能力很难完全处理各种错综复杂的问题并采取切实高效的行动。所有这些问题都需要团队成员互相关联共同合作建立羁绊来解决问题,统一思想,分工合作互相信,并进行必要的行动协调,开发团队的应变能力和持续的创作能力,依靠团队合作的力量创造奇迹。
6.团队期望
不求快,只求好。
第二篇
1.站立式会议
a.会议照片
b.工作表格
成员 |
昨天已完成的工作 |
今天计划完成的工作 |
工作中遇到的困难 |
蔡鑫源 |
对APP的界面进行美化 |
部分接口实现 |
无 |
纪培浩 |
数据库设计 |
注册代码实现 |
无 |
罗汉光 |
定义各功能模块接口 |
注册界面设计 |
无 |
拜合提亚尔 |
制定组装测试计划 |
对已完成的文档进行评审 |
无 |
2.项目燃尽图
![](https://img2020.cnblogs.com/blog/2530348/202111/2530348-20211127231442080-284384564.png)
3.代码签入
package controller
import (
"github.com/gin-gonic/gin"
"net/http"
"software_bottle/model"
)
//注册
func Register(c *gin.Context) {
var user model.User
err := c.BindJSON(&user)
if len(user.UserName) > 24 {
c.JSON(http.StatusInternalServerError, gin.H{
"code": http.StatusInternalServerError,
"msg": "username can't more than eight Chinese characters",
})
return
}
if len(user.Password) < 8 || len(user.Password) > 15 {
c.JSON(http.StatusInternalServerError, gin.H{
"code": http.StatusInternalServerError,
"msg": "password's length should between 8 to 15",
})
return
}
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": http.StatusInternalServerError,
"msg": err.Error(),
})
return
}
//用户名已存在
if err = model.UserIsExists(user.UserName); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": http.StatusInternalServerError,
"msg": err.Error(),
})
return
}
user.ImagePath = model.IMAG_PATH + model.DefaultImg
if err = model.CreateAUser(&user); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": http.StatusInternalServerError,
"msg": err.Error(),
})
} else {
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": "Success",
"user_id": user.UserId,
//"data": user,
})
}
}
4.模块的最新截图
![](https://img2020.cnblogs.com/blog/2530348/202111/2530348-20211127235407869-2082923796.png)
5.每人总结
成员 |
总结 |
蔡鑫源 |
革命仍未胜利 |
纪培浩 |
加油! |
罗汉光 |
今天很充实 |
拜合提亚尔 |
加油! |
第三篇
1.站立式会议
a.会议照片
b.工作表格
成员 |
昨天已完成的工作 |
今天计划完成的工作 |
工作中遇到的困难 |
蔡鑫源 |
部分接口实现 |
上传代码、完成博客随笔 |
无 |
纪培浩 |
注册代码实现 |
登录代码实现 |
无 |
罗汉光 |
注册界面设计 |
登录界面设计 |
无 |
拜合提亚尔 |
对已完成的文档进行评审 |
制定模块测试方案 |
无 |
2.项目燃尽图
![](https://img2020.cnblogs.com/blog/2530348/202111/2530348-20211127231501193-107751098.png)
3.代码签入
package controller
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"io"
"net/http"
"os"
"software_bottle/model"
"time"
)
const (
SecretKey = "XXXXXXXXXXX123456"
)
type User struct {
Username string `json:"user_name"`
Password string `json:"password"`
}
type Token struct {
Token string `json:"token"`
}
//登录
func Login(c *gin.Context) {
var user User
err := c.BindJSON(&user)
//获取json失败
if err != nil {
c.JSON(http.StatusForbidden, gin.H{
"code": http.StatusForbidden,
"msg": "error in decode request",
})
return
}
var uuser model.User
//查不到记录
if err = model.GetAUser(&uuser, user.Username); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": http.StatusInternalServerError,
"msg": err.Error(),
})
return
} else {
//密码不对
if uuser.Password != user.Password {
c.JSON(http.StatusInternalServerError, gin.H{
"code": http.StatusInternalServerError,
"msg": errors.New("password error").Error(),
})
return
}
}
token := jwt.New(jwt.SigningMethodHS256)
var claims = make(jwt.MapClaims)
claims["UserName"] = uuser.UserName
claims["UserId"] = uuser.UserId
//token过期时间
claims["exp"] = time.Now().Add(time.Hour * time.Duration(24)).Unix()
//token创建时间
claims["iat"] = time.Now().Unix()
token.Claims = claims
//加密生成token
tokenString, err := token.SignedString([]byte(SecretKey))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": http.StatusInternalServerError,
"msg": "Sign token error",
})
return
}
//返回token
f, err := os.Open(uuser.ImagePath)
buf := bytes.NewBuffer(nil)
if _, err = io.Copy(buf, f); err != nil {
fmt.Println("failed to copy picture")
}
c.JSON(http.StatusOK, gin.H{
"code": http.StatusOK,
"msg": "Success",
"token": tokenString,
"img_base64": base64.StdEncoding.EncodeToString(buf.Bytes()),
})
}
func Safe(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"code": http.StatusOK,
"msg": "authorized success",
})
}
4.模块的最新截图
![](https://img2020.cnblogs.com/blog/2530348/202111/2530348-20211127235430270-1112091142.png)
5.每人总结
成员 |
总结 |
蔡鑫源 |
再接再厉 |
纪培浩 |
加油! |
罗汉光 |
又是充实的一天 |
拜合提亚尔 |
加油! |
第四篇
1.站立式会议
a.会议照片
b.工作表格
成员 |
昨天已完成的工作 |
今天计划完成的工作 |
工作中遇到的困难 |
蔡鑫源 |
上传代码、完成博客随笔 |
完成博客随笔 |
无 |
纪培浩 |
登录代码实现 |
网络连接 |
无 |
罗汉光 |
登录界面设计 |
漂流瓶界面设计 |
无 |
拜合提亚尔 |
制定模块测试方案 |
进行模块测试和调试 |
无 |
2.项目燃尽图
![](https://img2020.cnblogs.com/blog/2530348/202111/2530348-20211127231538435-1999230638.png)
3.代码签入
package com.example.driftbottle.ui.meet;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ShapeDrawable;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.PopupWindow;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.resource.bitmap.CircleCrop;
import com.bumptech.glide.request.RequestOptions;
import com.example.driftbottle.MyApplication;
import com.example.driftbottle.R;
import com.example.driftbottle.chat.ChatActivity;
import com.example.driftbottle.database.AppDatabase;
import com.example.driftbottle.database.Talk;
import com.example.driftbottle.database.TalkDao;
import com.example.driftbottle.databinding.FragmentMeetBinding;
import com.example.driftbottle.loginorregister.LoginOrRegisterActivity;
import com.example.driftbottle.main.MainActivity;
import com.example.driftbottle.net.BottleMessage;
import com.example.driftbottle.net.MyService;
import com.example.driftbottle.util.ToastUtil;
import com.google.gson.JsonObject;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.util.List;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
/**
* A simple {@link Fragment} subclass.
* Use the {@link MeetFragment#newInstance} factory method to
* create an instance of this fragment.
*/
public class MeetFragment extends Fragment {
private static final String TAG = "MeetFragment";
private FragmentMeetBinding binding = null;
private FragmentActivity activity = null;
private ViewGroup rootView = null;
private int MODE_THROW = 0;
private int MODE_PICk = 1;
private int pickNum = 0;
private final int UPDATE_HEAD = 2;
// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private static final String ARG_PARAM1 = "param1";
private static final String ARG_PARAM2 = "param2";
// TODO: Rename and change types of parameters
private String mParam1;
private String mParam2;
private TextView letterTextView;
private EditText letterEditText;
private RelativeLayout rl;
private MainActivity mainActivity;
private ProgressBar progressBar;
private Handler handler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
@Override
public boolean handleMessage(@NonNull Message msg) {
switch (msg.what){
case UPDATE_HEAD:
MyApplication application = (MyApplication) activity.getApplication();
AppDatabase appDatabase = application.getmAppDatabase();
TalkDao talkDao = appDatabase.getTalkDao();
// List<Talk> list = talkDao.getTalkById(userId);
// if (list.size()==0){
// appDatabase.getTalkDao().insertAll(new Talk(0,userId,userName,base64));
// }else if (list.size()==1){
// Talk old = list.get(0);
// if (!old.getBase64().equals(base64)){
// old.setBase64(base64);
// talkDao.update(old);
// }
// }
break;
default:
break;
}
return false;
}
});
public MeetFragment() {
// Required empty public constructor
}
public static MeetFragment newInstance() {
MeetFragment fragment = new MeetFragment();
Bundle args = new Bundle();
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
mParam1 = getArguments().getString(ARG_PARAM1);
mParam2 = getArguments().getString(ARG_PARAM2);
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentMeetBinding.inflate(inflater,container,false);
activity = getActivity();
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
rootView = (ViewGroup) view;
Log.d(TAG, "=== rootView--> "+rootView);
setPickBottle();
View.OnClickListener listener = setBottleListener(R.layout.popup_window_bg,R.id.btn_letter_cancel,R.id.btn_letter_send,R.id.img_letter_head,R.id.text_letter_name,R.id.edittext_letter,MODE_THROW);
binding.imgNewBottle.setOnClickListener(listener);
}
private View.OnClickListener setBottleListener(int popup_window_bg, int btn_letter_cancel, int btn_letter_send, int img_letter_head,int userNameTextViewId,int editTextId,int mode) {
View.OnClickListener listener = new View.OnClickListener() {
@Override
public void onClick(View v) {
SharedPreferences sharedPreferences = activity.getSharedPreferences("data",Context.MODE_PRIVATE);
String myName = sharedPreferences.getString("userName","用户名");
String base64 = sharedPreferences.getString("base64",null);
int myId = sharedPreferences.getInt("userId",-1);
showPopupWindow(popup_window_bg,btn_letter_cancel,btn_letter_send,img_letter_head,userNameTextViewId,editTextId,null,myName,base64,mode,myId);
}
};
return listener;
}
private View.OnClickListener setPickBottleListener(){
View.OnClickListener listener = new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
mainActivity = (MainActivity) activity;
mainActivity.refuseClick();
progressBar = new ProgressBar(activity);
RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(200,200);
lp.addRule(RelativeLayout.CENTER_IN_PARENT);
rl = (RelativeLayout) rootView;
rl.addView(progressBar,lp);
v.setVisibility(View.INVISIBLE);
addPickNum();
SharedPreferences sharedPreferences = v.getContext().getSharedPreferences("data", Context.MODE_PRIVATE);
String token = sharedPreferences.getString("token",null);
Call<BottleMessage> call = MyService.getInstance().getBottle(token);
call.enqueue(new Callback<BottleMessage>() {
@Override
public void onResponse(Call<BottleMessage> call, Response<BottleMessage> response) {
int code = response.code();
if (code==200){
String base64 = response.body().getImg_base64();
String message = response.body().getMessage();
String userName = response.body().getUser_name();
int userId = response.body().getUser_id();
try {
showPopupWindow(R.layout.popup_window_bg2,R.id.btn_receive_letter_throw_back,
R.id.btn_receive_letter_respond,R.id.img_receive_letter_head,
R.id.text_receive_letter_name,R.id.text_receive_letter,message,userName,base64,MODE_PICk,userId);
}catch (Exception e){
Log.d(TAG, "=== e--> "+e);
}
//如果需要更新头像
Message message1 = new Message();
message1.what = UPDATE_HEAD;
handler.sendMessage(message1);
}else if (code==401){
ToastUtil.getShortToast(activity,"token过期").show();
}else{
ToastUtil.getShortToast(activity,"未知错误").show();
}
}
@Override
public void onFailure(Call<BottleMessage> call, Throwable t) {
rl.removeView(progressBar);
mainActivity.recoverClickable();
}
});
}catch (Exception e){
Log.d(TAG, "=== e--> "+e);
}
}
};
return listener;
}
private void showPopupWindow(int layoutId,int leftBtnId,int rightBtnId,int imageViewId,
int userNameTextViewId,int editTextId, String message,String userName,
String base64,int mode,int userId){
if (rl!=null){
rl.removeView(progressBar);
}
if (mainActivity!=null){
mainActivity.recoverClickable();
}
//变暗
WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
lp.alpha = 0.3f;
activity.getWindow().setAttributes(lp);
//充入布局,测量
View contentView = getLayoutInflater().inflate(layoutId,null);
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(rootView.getMeasuredWidth(), View.MeasureSpec.AT_MOST);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(rootView.getMeasuredHeight(), View.MeasureSpec.AT_MOST);
contentView.measure(widthMeasureSpec,heightMeasureSpec);
//下面这句不能用只用一个view参数的接口,
PopupWindow popupWindow = new PopupWindow(contentView, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
popupWindow.setFocusable(true);
popupWindow.setTouchInterceptor(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
double width = v.getWidth();
double height = v.getHeight();
double x = event.getX();
double y = event.getY();
if (x>0 && x<width && y>0 && y<height){
//在弹窗内,需要返回false,否则弹窗内事件无法触发
return false;
}
return true;
}
});
popupWindow.setOnDismissListener(new PopupWindow.OnDismissListener() {
@Override
public void onDismiss() {
WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
lp.alpha = 1f;
activity.getWindow().setAttributes(lp);
}
});
TextView userNameText = contentView.findViewById(userNameTextViewId);
userNameText.setText(userName);
ImageView headImg = contentView.findViewById(imageViewId);
if (base64!=null && base64.startsWith("data")){
base64 = base64.split(",")[1];
}
byte[] decode = Base64.decode(base64,Base64.DEFAULT);
Bitmap bitmap = BitmapFactory.decodeByteArray(decode,0,decode.length);
Glide.with(MeetFragment.this).load(bitmap).apply(RequestOptions.bitmapTransform(new CircleCrop())).into(headImg);
letterTextView = null;
letterEditText = null;
if (mode == MODE_PICk){
letterTextView = contentView.findViewById(editTextId);
letterTextView.setText(message);
}else if (mode == MODE_THROW){
letterEditText = contentView.findViewById(editTextId);
}
Button throwBackBtn = contentView.findViewById(leftBtnId);
if (throwBackBtn!=null){
throwBackBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d(TAG, "=== onClick: dismiss");
popupWindow.dismiss();
}
});
}
Button respondBtn = contentView.findViewById(rightBtnId);
if (respondBtn!=null){
String finalBase6 = base64;
respondBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mode == MODE_PICk){
//TODO
Intent intent = new Intent(activity, ChatActivity.class);
intent.putExtra("userName",userName);
intent.putExtra("base64", finalBase6);
intent.putExtra("userId",userId);
startActivity(intent);
}else if (mode == MODE_THROW){
if (letterEditText==null) return;
String newMessage = letterEditText.getText().toString();
if (TextUtils.isEmpty(newMessage)){
ToastUtil.getShortToast(activity, "内容不能为空").show();
return;
}
SharedPreferences sharedPreferences = activity.getSharedPreferences("data",Context.MODE_PRIVATE);
String token = sharedPreferences.getString("token","1");
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("message",newMessage);
Log.d(TAG, "=== message--> "+newMessage);
Call<ResponseBody> call = MyService.getInstance().throwBottle(token,jsonObject);
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
int code = response.code();
Log.d(TAG, "=== response code--> "+code);
if (code == 200){
ToastUtil.getShortToast(v.getContext(),"漂流瓶已扔出").show();
}else if (code==401){
ToastUtil.getShortToast(v.getContext(),"token过期").show();
}else{
ToastUtil.getShortToast(v.getContext(),"未知错误").show();
}
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
ToastUtil.getShortToast(v.getContext(),"未知错误").show();
}
});
}
popupWindow.dismiss();
}
});
}
int x = rootView.getWidth()/2-contentView.getMeasuredWidth()/2;
int y = rootView.getHeight()/2-contentView.getMeasuredHeight()/2;
popupWindow.showAtLocation(rootView,Gravity.NO_GRAVITY,x,y);
}
private void addPickNum(){
pickNum++;
if (pickNum==5){
binding.imgBottle1.setVisibility(View.VISIBLE);
binding.imgBottle2.setVisibility(View.VISIBLE);
binding.imgBottle3.setVisibility(View.VISIBLE);
binding.imgBottle4.setVisibility(View.VISIBLE);
binding.imgBottle5.setVisibility(View.VISIBLE);
pickNum = 0;
}
}
private void setPickBottle(){
Log.d(TAG, "=== setPickBottle: ");
View.OnClickListener listener = setPickBottleListener();
binding.imgBottle1.setOnClickListener(listener);
binding.imgBottle2.setOnClickListener(listener);
binding.imgBottle3.setOnClickListener(listener);
binding.imgBottle4.setOnClickListener(listener);
binding.imgBottle5.setOnClickListener(listener);
}
}
4.模块的最新截图
5.每人总结
成员 |
总结 |
蔡鑫源 |
好多不会 |
纪培浩 |
加油! |
罗汉光 |
还是充实的一天 |
拜合提亚尔 |
加油! |
第五篇
1.站立式会议
a.会议照片
b.工作表格
成员 |
昨天已完成的工作 |
今天计划完成的工作 |
工作中遇到的困难 |
蔡鑫源 |
完成博客随笔 |
界面美化、完成博客随笔 |
无 |
纪培浩 |
网络连接 |
数据处理 |
无 |
罗汉光 |
漂流瓶界面设计 |
聊天界面设计 |
无 |
拜合提亚尔 |
进行模块测试和调试 |
对实现过程及已完成的文档进行评审 |
无 |
2.项目燃尽图
![](https://img2020.cnblogs.com/blog/2530348/202111/2530348-20211127231610441-671079339.png)
3.代码签入
package controller
import (
"bytes"
"encoding/base64"
"fmt"
"github.com/gin-gonic/gin"
"github.com/gomodule/redigo/redis"
"io"
"math/rand"
"net/http"
"os"
"software_bottle/dao"
"software_bottle/model"
"strconv"
"strings"
"time"
)
func SendImg(c *gin.Context) {
userId, ok := c.Get("user_id")
if !ok {
}
var user model.User
err := model.GetAUserById(&user, int(userId.(float64)))
if err != nil {
}
//拷贝图片
f, err := os.Open(user.ImagePath)
buf := bytes.NewBuffer(nil)
if _, err = io.Copy(buf, f); err != nil {
fmt.Println(err.Error())
}
//获取图片格式
slice := strings.Split(user.ImagePath, ".")
ext := slice[len(slice)-1]
c.JSON(http.StatusOK, gin.H{
"code": http.StatusOK,
"msg": "success get Img",
"img_base64": "data:image/" + ext + ";base64," + base64.StdEncoding.EncodeToString(buf.Bytes()),
"img_type": ext,
})
}
func SendBottle(c *gin.Context) {
conn := dao.RedisCache.Conn()
defer conn.Close()
id, _ := c.Get("user_id")
userId := strconv.Itoa(int(id.(float64)))
//删除过期漂流瓶
DelExpireMembers(conn)
rand.Seed(time.Now().Unix())
//随机获取一个漂流瓶
num, _ := redis.Int64(conn.Do("ZCARD", dao.Field[2]))
//当存在漂流瓶时
if num > 0 {
//尝试100次,直到拿到其他人发送的漂流瓶则直接返回,否则在100次没取到之后就返回无漂流瓶可取.
for i := 0; i < 100; i++ {
index := rand.Int63n(num)
val, err := redis.Values(conn.Do("ZRANGE", dao.Field[2], index, index, "WITHSCORES"))
key := string(val[0].([]byte))
//漂流瓶owner的ID
s := strings.Split(key, "-")[0]
//防止获取到自己的漂流瓶
if s == userId {
continue
}
senderId, _ := strconv.Atoi(s)
var sender model.User
_ = model.GetAUserById(&sender, senderId)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": http.StatusInternalServerError,
"msg": "获取漂流瓶失败",
})
return
}
value, err := redis.Values(conn.Do("HMGET", key, dao.Field[0]))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": http.StatusInternalServerError,
"msg": "获取漂流瓶内容失败",
})
return
}
imagePath := sender.ImagePath
msg := string(value[0].([]byte))
f, err := os.Open(imagePath)
buf := bytes.NewBuffer(nil)
if _, err = io.Copy(buf, f); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": http.StatusInternalServerError,
"msg": "获取漂流瓶用户头像失败",
})
return
}
slice := strings.Split(sender.ImagePath, ".")
ext := slice[len(slice)-1]
c.JSON(http.StatusOK, gin.H{
"code": http.StatusOK,
"message": msg,
"user_name": sender.UserName,
"user_id": sender.UserId,
"img_base64": "data:image/" + ext + ";base64," + base64.StdEncoding.EncodeToString(buf.Bytes()),
"img_type": ext,
})
return
}
}
c.JSON(http.StatusOK, gin.H{
"code": http.StatusInternalServerError,
"msg": "目前没有漂流瓶可以捞,请稍后重试",
})
}
func DelExpireMembers(conn redis.Conn) {
stop := time.Now().Unix() - EXPIRE
//fmt.Println(stop)
//删除一个月以前的漂流瓶
_, _ = conn.Do("ZREMRANGEBYSCORE", dao.Field[2], 0, stop)
}
package controller
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
"software_bottle/dao"
"software_bottle/model"
"strconv"
"time"
)
/*
redis Hash(存放漂流瓶信息,头像)
key:user_id-时间戳
field0:msg
value:message
redis sorted set(存放漂流瓶key)
key:bottle_owner
member:user_id-时间戳
随机取,如果漂流瓶得到回复,就从redis中删除,回复的话就建立一对一聊天了
一对一聊天
如果对方离线,则把消息方入redis中,等对方上线再从redis中取出发送,然后删除redis
*/
type Bottle struct {
Msg string `json:"message"`
}
const (
EXPIRE = int64(1000 * 3600 * 24 * 30)
)
func UploadBottle(c *gin.Context) {
var bottle Bottle
err := c.BindJSON(&bottle)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": http.StatusInternalServerError,
"msg": "can not get message:" + err.Error(),
})
return
}
fmt.Println(bottle)
//int
v, ok := c.Get("user_id")
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{
"code": http.StatusInternalServerError,
"msg": "user_id isn't exists",
})
return
}
userId := int(v.(float64))
cur := time.Now().Unix()
key := strconv.Itoa(userId) + "-" + strconv.FormatInt(cur, 10)
var user model.User
if err = model.GetAUserById(&user, userId); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": http.StatusInternalServerError,
"msg": err.Error(),
})
return
}
conn := dao.RedisCache.Conn()
defer conn.Close()
//事务
conn.Send("MULTI")
conn.Send("HSET", key, dao.Field[0], bottle.Msg)
conn.Send("ZADD", dao.Field[2], cur, key)
conn.Send("EXPIRE", key, EXPIRE)
conn.Send("PERSIST", dao.Field[2])
_, err = conn.Do("EXEC")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": http.StatusInternalServerError,
"msg": "redis save error:" + err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": http.StatusOK,
"msg": "send bottle success",
})
}
4.模块的最新截图
![](https://img2020.cnblogs.com/blog/2530348/202111/2530348-20211127235450546-2110338585.png)
5.每人总结
成员 |
总结 |
蔡鑫源 |
博客写的更快了 |
纪培浩 |
加油! |
罗汉光 |
充实! |
拜合提亚尔 |
加油! |
第六篇
1.站立式会议
a.会议照片
b.工作表格
成员 |
昨天已完成的工作 |
今天计划完成的工作 |
工作中遇到的困难 |
蔡鑫源 |
界面美化、完成博客随笔 |
实现部分接口 |
无 |
纪培浩 |
数据处理 |
优化算法 |
无 |
罗汉光 |
聊天界面设计 |
服务端开发 |
无 |
拜合提亚尔 |
对实现过程及已完成的文档进行评审 |
执行集成测试计划 |
无 |
2.项目燃尽图
![](https://img2020.cnblogs.com/blog/2530348/202111/2530348-20211127231630936-948492757.png)
3.代码签入
package controller
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/nfnt/resize"
"image/jpeg"
"image/png"
"io"
"log"
"net/http"
"os"
"path"
"software_bottle/model"
"strconv"
"strings"
"time"
)
//
func compressJPG(path string, quality int) {
file, err := os.Open(path)
if err != nil {
log.Fatal(err)
}
img, err := jpeg.Decode(file)
if err != nil {
log.Fatal(err)
}
file.Close()
m := resize.Resize(800, 0, img, resize.NearestNeighbor)
out, err := os.Create(path)
if err != nil {
log.Fatal(err)
}
defer out.Close()
jpeg.Encode(out, m, &jpeg.Options{Quality: quality})
}
func compressPNG(path string) {
file, err := os.Open(path)
if err != nil {
log.Fatal(err)
}
img, err := png.Decode(file)
if err != nil {
log.Fatal(err)
}
file.Close()
m := resize.Resize(800, 0, img, resize.NearestNeighbor)
out, err := os.Create(path)
if err != nil {
log.Fatal(err)
}
defer out.Close()
png.Encode(out, m)
}
func UploadImag(c *gin.Context) {
//解析图片,数据保存到本地,路径持久化到数据库
imgFile, imgHandler, err := c.Request.FormFile("img")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": http.StatusInternalServerError,
"msg": err.Error(),
})
return
}
defer func() {
_ = imgFile.Close()
}()
current := time.Now()
//时间戳命名图片,防止重复
ext := strings.ToLower(path.Ext(imgHandler.Filename))
if !(ext == ".jpg" || ext == ".png" || ext == ".jpeg") {
c.JSON(http.StatusInternalServerError, gin.H{
"code": http.StatusInternalServerError,
"msg": "图片格式只支持.jpg,.png,.jpeg",
})
return
}
imgPath := model.IMAG_PATH + strconv.FormatInt(current.Unix(), 10) + ext
f, err := os.Create(imgPath)
if err != nil {
log.Println(err.Error())
return
}
defer func() {
_ = f.Close()
}()
//将用户上传的图片保存到服务器本地
_, err = io.Copy(f, imgFile)
if err != nil {
uploadError(err, c)
return
}
fi, _ := f.Stat()
//压缩图片
if fi.Size() >= 1024000 {
if ext == ".png" {
compressPNG(imgPath)
} else {
compressJPG(imgPath, 100)
}
}
username, ok := c.Get("user_name")
if !ok {
fmt.Println("Not get username")
return
}
var user model.User
err = model.GetAUser(&user, username.(string))
if err != nil {
uploadError(err, c)
return
}
//删除服务器本地原本用户头像
if user.ImagePath != "" && user.ImagePath != (model.IMAG_PATH+"default.jpg") {
_ = os.Remove(user.ImagePath)
}
user.ImagePath = imgPath
err = model.UpdateUser(&user)
if err != nil {
uploadError(err, c)
return
}
c.JSON(http.StatusOK, gin.H{
"code": http.StatusOK,
"msg": "img post success",
})
}
func uploadError(err error, c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{
"code": http.StatusInternalServerError,
"msg": "uploadImg error:" + err.Error(),
})
}
4.模块的最新截图
5.每人总结
成员 |
总结 |
蔡鑫源 |
快胜利了 |
纪培浩 |
加油! |
罗汉光 |
今天也是充实的一天 |
拜合提亚尔 |
加油! |
第七篇
1.站立式会议
a.会议照片
b.工作表格
成员 |
昨天已完成的工作 |
今天计划完成的工作 |
工作中遇到的困难 |
蔡鑫源 |
实现部分接口 |
完成博客随笔 |
无 |
纪培浩 |
优化算法 |
客户端开发 |
无 |
罗汉光 |
服务端开发 |
接口的对接 |
无 |
拜合提亚尔 |
执行集成测试计划 |
测试整个软件系统(健壮性测试) |
无 |
2.项目燃尽图
![](https://img2020.cnblogs.com/blog/2530348/202111/2530348-20211127231640821-1031224909.png)
3.代码签入
package websocket
import (
"bytes"
"encoding/base64"
"encoding/json"
"github.com/gin-gonic/gin"
"github.com/gomodule/redigo/redis"
"io"
"net/http"
"os"
"software_bottle/dao"
"software_bottle/model"
"strconv"
"strings"
"github.com/gorilla/websocket"
"log"
)
type Hub struct {
register chan *Client
unregister chan *Client
msgChan chan []byte
clienlist map[int]*Client
}
type Client struct {
id int
conn *websocket.Conn
}
type MsgContent struct {
Sender int `json:"sender"`
Receiver int `json:"receiver"`
Message string `json:"message"`
}
type ReplyContent struct {
UserId int `json:"user_id"`
ImgBase64 string `json:"img_base64"`
Message string `json:"message"`
}
var hub = &Hub{
register: make(chan *Client),
unregister: make(chan *Client),
msgChan: make(chan []byte),
clienlist: make(map[int]*Client),
}
var upgrade = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
var Filed = []string{"user_id", "img_base64", "message"}
func (h *Hub) Run() {
for {
//依次为注册,注销,转发
select {
case c := <-h.register:
log.Printf("客户端 %d 加入\n", c.id)
h.clienlist[c.id] = c
case c := <-h.unregister:
log.Printf("客户端 %d 退出\n", c.id)
delete(h.clienlist, c.id)
case v := <-h.msgChan:
msg := &MsgContent{}
_ = json.Unmarshal(v, msg)
if client, ok := h.clienlist[msg.Receiver]; ok {
log.Printf("转发自客户端 %d 向客户端 %d 发送的消息: %s\n",
msg.Sender, msg.Receiver, msg.Message)
client.WriteMsg(v)
} else {
//接收方离线的处理,存在redis中
SaveOfflineMsg(v)
log.Println("用户已离线")
}
}
}
}
//获取离线时未接收的回复
func SaveOfflineMsg(v []byte) {
conn := dao.RedisCache.Conn()
defer conn.Close()
var reply ReplyContent
var msg MsgContent
_ = json.Unmarshal(v, &msg)
err := GetReply(v, &reply)
if err != nil {
log.Println("write OfflineMsg error:", err)
return
}
key := strconv.Itoa(msg.Receiver)
replyBytes, _ := json.Marshal(reply)
conn.Send("MULTI")
conn.Send("SADD", key, replyBytes)
_, err = conn.Do("EXEC")
if err != nil {
log.Println("save OfflineMsg in redis error:", err)
return
}
}
func (c *Client) WriteMsg(v []byte) {
var reply ReplyContent
err := GetReply(v, &reply)
if err != nil {
log.Println("write Msg error:", err)
return
}
replyBytes, _ := json.Marshal(reply)
err = c.conn.WriteMessage(websocket.TextMessage, replyBytes)
if err != nil {
log.Println("write Msg error:", err)
return
}
log.Printf("发送到客户端的消息: %s\n", v)
}
//将sender发送的消息进行处理
func GetReply(v []byte, reply *ReplyContent) error {
var msg MsgContent
_ = json.Unmarshal(v, &msg)
var user model.User
err := model.GetAUserById(&user, msg.Sender)
if err != nil {
//log.Println("获取用户信息失败", err.Error())
return err
}
imagePath := user.ImagePath
f, err := os.Open(imagePath)
buf := bytes.NewBuffer(nil)
if _, err = io.Copy(buf, f); err != nil {
//log.Println("获取头像失败", err.Error())
return err
}
slice := strings.Split(user.ImagePath, ".")
ext := slice[len(slice)-1]
reply.UserId = user.UserId
reply.Message = msg.Message
reply.ImgBase64 = "data:image/" + ext + ";base64," + base64.StdEncoding.EncodeToString(buf.Bytes())
return nil
}
func (c *Client) ReadMsg() {
defer func() {
hub.unregister <- c
_ = c.conn.Close()
}()
//客户端不断进行读取
for {
_, v, err := c.conn.ReadMessage()
if err != nil {
log.Printf("%d read Msg error:%s\n", c.id, err.Error())
break
}
//为消息添加上发送者的id
msg := &MsgContent{}
_ = json.Unmarshal(v, msg)
msg.Sender = c.id
v, _ = json.Marshal(msg)
//转发消息
hub.msgChan <- v
}
}
func WsHandle(c *gin.Context) {
go hub.Run()
conn, err := upgrade.Upgrade(c.Writer, c.Request, nil)
if err != nil {
log.Println(err)
return
}
v, _ := c.Get("user_id")
uid := int(v.(float64))
client := &Client{
id: uid,
conn: conn,
}
hub.register <- client
//为每个客户端建立一个读取协程
go client.ReadMsg()
}
func SendOfflineMsg(c *gin.Context) {
conn := dao.RedisCache.Conn()
defer conn.Close()
v, _ := c.Get("user_id")
userId := int(v.(float64))
key := strconv.Itoa(userId)
replyBytes, err := redis.Values(conn.Do("SMEMBERS", key))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": http.StatusOK,
"msg": err.Error(),
})
return
}
_, _ = conn.Do("DEL", key)
for _, val := range replyBytes {
//fmt.Println(index)
var reply ReplyContent
replyByte := val.([]byte)
err = json.Unmarshal(replyByte, &reply)
if err != nil {
//c.JSON(http.StatusInternalServerError, gin.H{
// "code": http.StatusInternalServerError,
// "msg": err.Error(),
//})
} else {
c.JSON(http.StatusOK, gin.H{
//"code": http.StatusOK,
"reply": reply,
})
}
}
}
4.模块的最新截图
5.每人总结
成员 |
总结 |
蔡鑫源 |
终于结束了 |
纪培浩 |
加油! |
罗汉光 |
最充实的一天了 |
拜合提亚尔 |
加油! |